Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
71dd48f
feat: update Dockerfile to include build-essential
Tyjfre-j Mar 19, 2026
cf721d2
feat: add GitHub Actions workflow for Docker image build and push
Tyjfre-j Mar 19, 2026
e771773
Merge remote-tracking branch 'origin/main' into feat/docker-ghcr
Tyjfre-j Mar 19, 2026
161eff1
chore: exclude env files from Docker build context
Tyjfre-j Mar 19, 2026
a3daa62
build: update system libs for headless OpenCV
Tyjfre-j Mar 19, 2026
5ee394e
deps: switch to opencv-python-headless
Tyjfre-j Mar 19, 2026
62ab60a
ci: publish linux/amd64 and linux/arm64 images
Tyjfre-j Mar 19, 2026
d1ab599
ci: update GHCR workflow and silence insightface mypy warning
Tyjfre-j Mar 19, 2026
9ec6201
Merge main into feat/docker-ghcr
Tyjfre-j Mar 20, 2026
91f358d
Add mobile session settings to config
Tyjfre-j Mar 20, 2026
42c4826
Use config for mobile session limits and TTL
Tyjfre-j Mar 20, 2026
f387d7a
Merge pull request #28 from MicroClub-USTHB/feat/docker-ghcr
Tyjfre-j Mar 20, 2026
80e7f0a
Add blocked column migration and sqlc updates
Tyjfre-j Mar 20, 2026
dc72df6
Add token blacklist and blocked checks in auth
Tyjfre-j Mar 20, 2026
b6ab722
Add admin user CRUD and block/unblock endpoints
Tyjfre-j Mar 20, 2026
058e1e4
Fix staff login crash on missing user
Tyjfre-j Mar 20, 2026
df123d8
Add admin and mobile session defaults to settings
Tyjfre-j Mar 20, 2026
22c5ca4
Refactor mobile auth endpoints for consistency
Tyjfre-j Mar 20, 2026
3f46dda
Refactor admin users router mappings and defaults
Tyjfre-j Mar 20, 2026
bbccb5c
Use settings and consistent DB error handling in user service
Tyjfre-j Mar 20, 2026
e534e32
Fix mypy exception handling in user service
Tyjfre-j Mar 20, 2026
cd516a1
refactor: move admin user mapping out of router
Tyjfre-j Mar 23, 2026
05c8102
feat: add admin user schema mapper
Tyjfre-j Mar 23, 2026
f4a6853
fix: rely on db for session validity in auth service
Tyjfre-j Mar 23, 2026
069ec7c
chore: deprecate token blacklist helpers
Tyjfre-j Mar 23, 2026
194e1df
fix: validate sessions via db in token auth
Tyjfre-j Mar 23, 2026
5aa0cb8
chore: remove token blacklist helpers
Tyjfre-j Mar 23, 2026
e5bafbb
Remove GH Actions cache from docker publish
Tyjfre-j Mar 23, 2026
1bad9f4
Use explicit image tags in docker publish workflow
Tyjfre-j Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ jobs:
build-and-push:
runs-on: ubuntu-latest
env:
IMAGE_NAME: ghcr.io/${{ github.repository }}
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set image name
shell: bash
run: |
echo "IMAGE_NAME=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
Expand All @@ -33,9 +34,9 @@ jobs:
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GHCR_PAT }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v5
Expand Down
7 changes: 7 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ class Settings(BaseSettings):
POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: int = 5432

# Mobile auth/session defaults
MOBILE_SESSION_LIMIT: int = 3
MOBILE_SESSION_TTL_SECONDS: int = 180
MOBILE_SESSION_DAYS: int = 7
# Admin list defaults
ADMIN_USERS_DEFAULT_LIMIT: int = 20
ADMIN_USERS_MAX_LIMIT: int = 100
# Security
jwt_secret: str
jwt_algorithm: str = "HS256"
Expand Down
2 changes: 2 additions & 0 deletions app/deps/token_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ async def get_current_mobile_user(
user = await container.auth_service.user_querier.get_user_by_id(id=session.user_id)
if not user:
raise HTTPException(status_code=401, detail="User not found")
if user.blocked:
raise HTTPException(status_code=403, detail="User is blocked")

return MobileUserSchema(
user_id=user.id,
Expand Down
22 changes: 6 additions & 16 deletions app/router/mobile/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from uuid import UUID

from app.container import get_container, Container
from app.core.exceptions import AppException
from app.deps.token_auth import MobileUserSchema, get_current_mobile_user

from app.schema.request.mobile.auth import MobileAuthRequest, RefreshTokenRequest
Expand All @@ -18,7 +17,6 @@ async def mobile_register_login(
req: MobileAuthRequest,
container: Container = Depends(get_container),
) -> MobileAuthResponse:

return await container.auth_service.mobile_register_login(container.redis, req)


Expand All @@ -27,20 +25,18 @@ async def refresh_token(
req: RefreshTokenRequest,
container: Container = Depends(get_container),
) -> MobileAuthResponse:

return await container.auth_service.refresh_token(container.redis, req.refresh_token)


@router.post("/logout")
async def logout(
container: Container = Depends(get_container),
User: MobileUserSchema = Depends(get_current_mobile_user),
current_user: MobileUserSchema = Depends(get_current_mobile_user),
) -> dict[str, str]:

return await container.auth_service.logout(
container.redis,
str(User.user_id),
str(User.session_id),
str(current_user.user_id),
str(current_user.session_id),
)


Expand All @@ -50,7 +46,6 @@ async def revoke_device(
container: Container = Depends(get_container),
current_user: MobileUserSchema = Depends(get_current_mobile_user),
) -> dict[str, str]:

await container.device_service.revoke_device(
device_id=device_id,
user_id=current_user.user_id,
Expand All @@ -63,17 +58,14 @@ async def get_me(
current_user: MobileUserSchema = Depends(get_current_mobile_user),
container: Container = Depends(get_container),
) -> MeResponse:

user = await container.auth_service.user_querier.get_user_by_id(id=current_user.user_id)
if user is None :
raise AppException.not_found("user not found")
user = await container.auth_service.get_user(user_id=current_user.user_id)

devices, _ = await container.device_service.get_all_devices(current_user.user_id)
device_list = [
DeviceSchema(
id=d.id,
device_name=d.device_name or "uknown ",
device_type=d.device_type or "uknown ",
device_name=d.device_name or "unknown",
device_type=d.device_type or "unknown",
totp_secret=d.totp_secret,
)
for d in devices
Expand All @@ -92,8 +84,6 @@ async def get_me(
expires_at=sessions_objs.expires_at,
)



return MeResponse(
user=UserSchema(id=user.id, email=user.email),
devices=device_list,
Expand Down
2 changes: 2 additions & 0 deletions app/router/web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from app.router.web.staff_users import router as staff_users_router
from app.router.web.event import router as event_router
from app.router.web.auth import router as auth_routes
from app.router.web.users import router as users_router

router = APIRouter(prefix="/admin", tags=["admin"])
router.include_router(staff_users_router)
router.include_router(event_router)
router.include_router(auth_routes)
router.include_router(users_router)
108 changes: 108 additions & 0 deletions app/router/web/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from uuid import UUID

from fastapi import APIRouter, Depends, Query, status

from app.container import Container, get_container
from app.core.config import settings
from app.core.logger import logger
from app.deps.cookie_auth import get_current_staff_user
from app.schema.request.web.user import AdminUserCreateRequest, AdminUserUpdateRequest
from app.schema.response.web.user import AdminUserSchema, to_admin_user_schema
from db.generated.models import StaffUser


router = APIRouter(prefix="/users")

@router.post("/", response_model=AdminUserSchema, status_code=status.HTTP_201_CREATED)
async def create_user(
req: AdminUserCreateRequest,
current_staff_user: StaffUser = Depends(get_current_staff_user),
container: Container = Depends(get_container),
) -> AdminUserSchema:
user = await container.auth_service.create_user(
email=req.email,
password=req.password,
display_name=req.display_name,
blocked=req.blocked,
)
logger.info("admin %s created user %s", current_staff_user.id, user.id)
return to_admin_user_schema(user)


@router.get("/", response_model=list[AdminUserSchema])
async def list_users(
limit: int = Query(
settings.ADMIN_USERS_DEFAULT_LIMIT, ge=1, le=settings.ADMIN_USERS_MAX_LIMIT
),
offset: int = Query(0, ge=0),
current_staff_user: StaffUser = Depends(get_current_staff_user),
container: Container = Depends(get_container),
) -> list[AdminUserSchema]:
users = await container.auth_service.list_users(limit=limit, offset=offset)
return [to_admin_user_schema(user) for user in users]


@router.get("/{user_id}", response_model=AdminUserSchema)
async def get_user(
user_id: UUID,
current_staff_user: StaffUser = Depends(get_current_staff_user),
container: Container = Depends(get_container),
) -> AdminUserSchema:
user = await container.auth_service.get_user(user_id=user_id)
return to_admin_user_schema(user)


@router.put("/{user_id}", response_model=AdminUserSchema)
async def update_user(
user_id: UUID,
req: AdminUserUpdateRequest,
current_staff_user: StaffUser = Depends(get_current_staff_user),
container: Container = Depends(get_container),
) -> AdminUserSchema:
user = await container.auth_service.update_user(
user_id=user_id,
email=req.email,
display_name=req.display_name,
blocked=req.blocked,
)
logger.info("admin %s updated user %s", current_staff_user.id, user_id)
return to_admin_user_schema(user)


@router.delete("/{user_id}", response_model=AdminUserSchema)
async def delete_user(
user_id: UUID,
current_staff_user: StaffUser = Depends(get_current_staff_user),
container: Container = Depends(get_container),
) -> AdminUserSchema:
user = await container.auth_service.delete_user(
redis=container.redis,
user_id=user_id,
)
logger.info("admin %s deleted user %s", current_staff_user.id, user_id)
return to_admin_user_schema(user)


@router.post("/{user_id}/block", response_model=AdminUserSchema)
async def block_user(
user_id: UUID,
current_staff_user: StaffUser = Depends(get_current_staff_user),
container: Container = Depends(get_container),
) -> AdminUserSchema:
user = await container.auth_service.block_user(
redis=container.redis,
user_id=user_id,
)
logger.info("admin %s blocked user %s", current_staff_user.id, user_id)
return to_admin_user_schema(user)


@router.post("/{user_id}/unblock", response_model=AdminUserSchema)
async def unblock_user(
user_id: UUID,
current_staff_user: StaffUser = Depends(get_current_staff_user),
container: Container = Depends(get_container),
) -> AdminUserSchema:
user = await container.auth_service.unblock_user(user_id=user_id)
logger.info("admin %s unblocked user %s", current_staff_user.id, user_id)
return to_admin_user_schema(user)
15 changes: 15 additions & 0 deletions app/schema/request/web/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import Optional
from pydantic import BaseModel, EmailStr, Field


class AdminUserCreateRequest(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8)
display_name: Optional[str] = None
blocked: bool = False


class AdminUserUpdateRequest(BaseModel):
email: Optional[EmailStr] = None
display_name: Optional[str] = None
blocked: Optional[bool] = None
25 changes: 25 additions & 0 deletions app/schema/response/web/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from datetime import datetime
from uuid import UUID

from pydantic import BaseModel
from db.generated.models import User


class AdminUserSchema(BaseModel):
id: UUID
email: str
display_name: str | None
blocked: bool
created_at: datetime
updated_at: datetime


def to_admin_user_schema(user: User) -> AdminUserSchema:
return AdminUserSchema(
id=user.id,
email=user.email,
display_name=user.display_name,
blocked=user.blocked,
created_at=user.created_at,
updated_at=user.updated_at,
)
4 changes: 2 additions & 2 deletions app/service/face_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import asyncio
from typing import List, Literal, Optional, Sequence, Tuple, TypedDict

import cv2
import cv2 # type: ignore
import numpy as np
from insightface.app import FaceAnalysis # type: ignore
from insightface.app import FaceAnalysis # type: ignore[import-untyped]
from app.core.exceptions import AppException


Expand Down
4 changes: 2 additions & 2 deletions app/service/staff_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ async def admin_login(
) -> WebAuthResponse:
print("hello")
staff: StaffUser | None = await self.staff_user_querier.get_staff_user_by_email(email=email)
if staff is None or not verify_password(password, staff.password):
logger.info(f'user:{staff.email}') # type: ignore
if staff is None or not verify_password(password, staff.password):
logger.info("admin login failed for email %s", email)
raise AppException.unauthorized("Invalid email or password")


Expand Down
Loading