Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0c2e6a9
Add blocked column migration and sqlc updates
Tyjfre-j Mar 20, 2026
d5bf8dc
Add token blacklist and blocked checks in auth
Tyjfre-j Mar 20, 2026
924da2c
Add admin user CRUD and block/unblock endpoints
Tyjfre-j Mar 20, 2026
40f6a18
Fix staff login crash on missing user
Tyjfre-j Mar 20, 2026
aeb2eac
Add admin and mobile session defaults to settings
Tyjfre-j Mar 20, 2026
a1d054e
Refactor mobile auth endpoints for consistency
Tyjfre-j Mar 20, 2026
5c583b6
Refactor admin users router mappings and defaults
Tyjfre-j Mar 20, 2026
96ff5fd
Use settings and consistent DB error handling in user service
Tyjfre-j Mar 20, 2026
016ea29
Fix mypy exception handling in user service
Tyjfre-j Mar 20, 2026
46dff8e
refactor: move admin user mapping out of router
Tyjfre-j Mar 23, 2026
7d9d215
feat: add admin user schema mapper
Tyjfre-j Mar 23, 2026
0bf1e5b
fix: rely on db for session validity in auth service
Tyjfre-j Mar 23, 2026
0f0059e
chore: deprecate token blacklist helpers
Tyjfre-j Mar 23, 2026
7124ec1
fix: validate sessions via db in token auth
Tyjfre-j Mar 23, 2026
97ca343
chore: remove token blacklist helpers
Tyjfre-j Mar 23, 2026
6a2a858
Remove GH Actions cache from docker publish
Tyjfre-j Mar 23, 2026
ea71e2d
Use explicit image tags in docker publish workflow
Tyjfre-j Mar 23, 2026
0dab21a
fix .gitignore for firebase info
wailbentafat Mar 25, 2026
143cf1d
feat : add compute_event_embedding to FaceEmbeddingService (#31)
maya-ots Mar 25, 2026
b04c929
feat: add storage cleaner worker (#33)
wailbentafat Mar 25, 2026
297a707
Feat/ai jetstream listener (#30)
Tyjfre-j Mar 25, 2026
462ba03
feat:seperate repsonsabilites for single face detection
wailbentafat Mar 25, 2026
3bdcc3a
refactor the schema folder add intenral file
wailbentafat Mar 25, 2026
43d7884
refacor dto and logique of the service detection
wailbentafat Mar 25, 2026
4691baf
rename fucntion to follow convention and add readbailite
wailbentafat Mar 25, 2026
d43ec8b
Feat/staged upload review (#36)
ademboukabes Mar 27, 2026
5d1a97c
Refresh expired Google Drive access tokens automatically
ademboukabes Mar 27, 2026
b2d5375
refactor: optimize worker imports, update dependency management, and …
wailbentafat Mar 31, 2026
d8fcf9f
Feat/group photo processor worker (#37)
maya-ots Mar 31, 2026
383441b
refactor: update Redis initialization and initialize local variables …
wailbentafat Mar 31, 2026
009b24d
feat: implement unified photo worker infrastructure and update datab…
wailbentafat Apr 1, 2026
798e86a
test: add unit tests for PhotoWorker message processing logic
wailbentafat Apr 1, 2026
b923c92
refactor: consolidate photo processing logic into PhotoWorker and imp…
wailbentafat Apr 1, 2026
cf9000d
feat: trigger photo processing events upon approval and implement aut…
wailbentafat Apr 1, 2026
a86f036
test: add test coverage for cleanup event scheduling in photo worker
wailbentafat Apr 1, 2026
849515f
feat: implement photo approval workflow with decision service and mob…
wailbentafat Apr 1, 2026
cb25230
feat: add user ID and signup status to auth response and implement au…
wailbentafat Apr 1, 2026
7f9c630
feat: integrate audit logging into photo approval and upload request …
wailbentafat Apr 1, 2026
291a03b
feat: publish audit event upon completion of photo processing
wailbentafat Apr 1, 2026
c9feb3a
feat: add Google Drive folder browsing and file search functionality …
wailbentafat Apr 1, 2026
91f3154
feat: add processing job tracking, user photo listing, and system-wid…
wailbentafat Apr 1, 2026
f5f840f
feat: add photos router and implement ListEventPhotosForUser query fo…
wailbentafat Apr 1, 2026
8cc221c
feat: add endpoint and service method to retrieve paginated photos fo…
wailbentafat Apr 1, 2026
8811dd6
feat: implement photo auto-approval logic, add visibility updates, an…
wailbentafat Apr 1, 2026
fd57660
feat: update UserPhotoService to verify access via face matches and a…
wailbentafat Apr 1, 2026
acc5c24
Process Drive folder uploads asynchronously in a worker (#38)
ademboukabes Apr 11, 2026
95a3570
feat: update Dockerfile and makefile for staging checks, add Run Mode…
Tyjfre-j Apr 22, 2026
3edad3f
fix: fixed ruff and mypy errors
Tyjfre-j Apr 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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ totp_issuer=MultiAI

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:8000/staff/drive/callback
GOOGLE_REDIRECT_URI=http://127.0.0.1:8000/stuff/drive/callback
GOOGLE_OAUTH_SCOPES=https://www.googleapis.com/auth/drive.readonly openid email profile
FACE_ENCRYPTION_KEY=hkbribvfirirbvivbibvib
16 changes: 3 additions & 13 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,12 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha,prefix=

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
${{ env.IMAGE_NAME }}:latest
${{ env.IMAGE_NAME }}:${{ github.sha }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ db/schema.sql
multiai-c9380-firebase-adminsdk-fbsvc-cb6e5ce41b.json
db.txt

.venv
multiai-c9380-firebase-adminsdk-fbsvc-cb6e5ce41b.json
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ FROM python:3.12-slim
# Install PostgreSQL client libraries and build tools
RUN apt-get update \
&& apt-get install -y --no-install-recommends libpq-dev build-essential \
libglib2.0-0 libgfortran5 \
libglib2.0-0 libgfortran5 libgl1 libxext6 libsm6 libxrender1 libxcb1 \
&& rm -rf /var/lib/apt/lists/*

ENV PYTHONDONTWRITEBYTECODE=1
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
s7a 3idkom
# multAI Backend

Project run instructions are documented here:

- [Run Modes Guide](docs/RUN_MODES.md)

Use the guide to choose the right workflow:

- Backend Dev Mode (hot reload on host)
- Staging Mode (shared image-based containers)
- Backend Staging-Check Mode (local code in staging-like containers)
38 changes: 32 additions & 6 deletions app/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@
from app.service.staff_user import StaffUserService

from app.service.audit import AuditService
from app.service.photo_approval import PhotoApprovalService
from app.service.user_photo import UserPhotoService
from app.service.upload_requests import UploadRequestsService
from app.service.users import AuthService
from app.service.user_notification import UserNotificationService
from db.generated import devices as device_queries
from db.generated import photo_approvals as photo_approval_queries
from db.generated import processing_jobs as processing_job_queries
from db.generated import photo_faces as photo_face_queries
from db.generated import photos as photo_queries
from db.generated import session as session_queries
from db.generated import staff_drive_connections as staff_drive_queries
from db.generated import staff_notifications as staff_notification_queries
from db.generated import stuff_user as staff_user_queries
from db.generated import upload_request_groups as upload_request_group_queries
from db.generated import upload_request_photos as upload_request_photo_queries
from db.generated import upload_requests as upload_request_queries
from db.generated import user as user_queries
Expand Down Expand Up @@ -51,9 +57,13 @@ def __init__(
self.device_querier = device_queries.AsyncQuerier(conn)
self.staff_user_querier = staff_user_queries.AsyncQuerier(conn)
self.staff_drive_querier = staff_drive_queries.AsyncQuerier(conn)
self.upload_request_group_querier = upload_request_group_queries.AsyncQuerier(conn)
self.upload_request_querier = upload_request_queries.AsyncQuerier(conn)
self.upload_request_photo_querier = upload_request_photo_queries.AsyncQuerier(conn)
self.photo_querier = photo_queries.AsyncQuerier(conn)
self.photo_approval_querier = photo_approval_queries.AsyncQuerier(conn)
self.photo_face_querier = photo_face_queries.AsyncQuerier(conn)
self.processing_job_querier = processing_job_queries.AsyncQuerier(conn)
self.staff_notification_querier = staff_notification_queries.AsyncQuerier(conn)
self.notification_querier = notification_queries.AsyncQuerier(conn)
self.audit_querier = audit_queries.AsyncQuerier(conn)
Expand Down Expand Up @@ -93,29 +103,31 @@ def __init__(
)
self.staged_upload_storage_service = StagedUploadStorageService()

self.audit_service = AuditService(
audit_querier=self.audit_querier,
user_querier=self.user_querier,
)

self.upload_requests_service = UploadRequestsService(
upload_request_group_querier=self.upload_request_group_querier,
upload_request_querier=self.upload_request_querier,
upload_request_photo_querier=self.upload_request_photo_querier,
photo_querier=self.photo_querier,
staged_upload_storage=self.staged_upload_storage_service,
staff_drive_service=self.staff_drive_service,
staff_notifications_service=self.staff_notifications_service,
audit_service=self.audit_service,
)

notification_queue = NotificationQueue(settings=NotifSetting)

self.user_notifications_service = UserNotificationService(
notification_querier=self.notification_querier,
notification_queue=notification_queue,
)

self.audit_service = AuditService(
audit_querier=self.audit_querier,
user_querier=self.user_querier,
device_querier=self.device_querier,
)

self.staff_user_service = StaffUserService()

self.staff_user_service.init(
staff_user_querier=self.staff_user_querier,)

Expand All @@ -124,6 +136,20 @@ def __init__(
p_querier=self.participant_querier,
)

self.photo_approval_service = PhotoApprovalService(
photo_approval_querier=self.photo_approval_querier,
photo_querier=self.photo_querier,
storage_service=self.staged_upload_storage_service,
audit_service=self.audit_service,
)

self.user_photo_service = UserPhotoService(
photo_querier=self.photo_querier,
photo_face_querier=self.photo_face_querier,
photo_approval_querier=self.photo_approval_querier,
staff_drive_service=self.staff_drive_service,
)

async def get_container(
conn: sqlalchemy.ext.asyncio.AsyncConnection = Depends(get_db),
) -> Container:
Expand Down
39 changes: 32 additions & 7 deletions app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pydantic_settings import BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import field_validator


class Settings(BaseSettings):
Expand All @@ -16,13 +17,13 @@ class Settings(BaseSettings):
NATS_HOST: str
NATS_PASSWORD: str
NATS_USER: str


# MinIO
MINIO_API_PORT: int
MINIO_ROOT_USER: str
MINIO_ROOT_PASSWORD: str
MINIO_HOST: str
MINIO_RETRY_ATTEMPTS: int = 3
MINIO_RETRY_BASE_SECONDS: float = 0.5

# PostgreSQL
POSTGRES_USER: str
Expand All @@ -35,13 +36,22 @@ class Settings(BaseSettings):
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"
encryption_key: str
totp_issuer: str = "multAI"

# Face embedding model
FACE_EMBEDDING_MODEL_NAME: str = "buffalo_l"
FACE_EMBEDDING_PROVIDERS: str = "CPUExecutionProvider"
FACE_EMBEDDING_CTX_ID: int = -1
FACE_EMBEDDING_DET_WIDTH: int = 640
FACE_EMBEDDING_DET_HEIGHT: int = 640

# Google Drive OAuth
GOOGLE_CLIENT_ID: str = ""
GOOGLE_CLIENT_SECRET: str = ""
Expand All @@ -53,9 +63,24 @@ class Settings(BaseSettings):
FACE_ENCRYPTION_KEY: str
FIREBASE_CREDENTIALS_PATH: str = "multiai-c9380-firebase-adminsdk-fbsvc-cb6e5ce41b.json"

class Config:
env_file = ".env"
extra = "ignore"
model_config = SettingsConfigDict(
env_file=".env",
extra="ignore",
)

@field_validator("debug", mode="before")
@classmethod
def _parse_debug(cls, value): # type: ignore[no-untyped-def]
if value is None:
return True
if isinstance(value, str):
lowered = value.strip().lower()
if lowered in {"release", "prod", "production", "false", "0", "no"}:
return False
if lowered in {"true", "1", "yes"}:
return True
return value
return value


settings = Settings() # type: ignore
22 changes: 21 additions & 1 deletion app/core/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ class RedisKey(str, Enum):

NOTIFICATION_EVENT_SUBJECT = "notification_event"
AUDIT_EVENT_SUBJECT = "audit.event"
FINAL_BUCKET_CLEANUP_SUBJECT = "ai.final_bucket.completed"
FINAL_BUCKET_CLEANUP_STREAM = "ai-final-bucket-cleanup"
FINAL_BUCKET_CLEANUP_DURABLE_NAME = "ai-final-bucket-cleaner"
UPLOAD_GROUP_IMPORT_SUBJECT = "staff.upload_group.import.requested"
UPLOAD_GROUP_IMPORT_STREAM = "staff-upload-group-import"
UPLOAD_GROUP_IMPORT_DURABLE_NAME = "staff-upload-group-import-worker"


class AuditEventType(str, Enum):
Expand All @@ -18,7 +24,8 @@ class AuditEventType(str, Enum):
UPLOAD_REQUEST_CREATED = "upload_request.created"
UPLOAD_REQUEST_APPROVED = "upload_request.approved"
UPLOAD_REQUEST_REJECTED = "upload_request.rejected"

PHOTO_PROCESSED = "photo.processed"
PHOTO_APPROVAL_DECIDED = "photo_approval.decided"


IMAGE_ALLOWED_TYPES = {
Expand All @@ -28,6 +35,19 @@ class AuditEventType(str, Enum):
"image/heif"
}

DEFAULT_CONTENT_TYPE = "application/octet-stream"
DRIVE_ALLOWED_HOSTS = {"drive.google.com", "docs.google.com"}
MINIO_URL_PREFIX = "minio://"

IMAGES_BUCKET_NAME = "images"
DOCUMENTS_BUCKET_NAME = "documents"
WA_SIM_BUCKET_NAME = "wa-sim"

GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo"
GOOGLE_DRIVE_FILES_URL = "https://www.googleapis.com/drive/v3/files/{file_id}"

MAX_IMAGE_SIZE = 5 * 1024 * 1024
MIN_ENROLL_IMAGES = 3
MAX_ENROLL_IMAGES = 5
3 changes: 3 additions & 0 deletions app/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def handle_check_violation(exc: Exception) -> HTTPException:
def handle(exc: Exception) -> HTTPException:
logger.error("Database error: %s", exc)

if isinstance(exc, HTTPException):
return exc

if isinstance(exc, IntegrityError):
orig = getattr(exc, "orig", None)
sqlstate = getattr(orig, "sqlstate", None)
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
Loading