#!/usr/bin/env bash set -euo pipefail write() { local path="$1"; shift mkdir -p "$(dirname "$path")" if [[ -f "$path" ]]; then cp -f "$path" "${path}.bak"; fi cat >"$path" <<'PYEOF' '"$@"' PYEOF echo "✔ wrote $path" } # ---------- chat/src/app/api/chat.py ---------- write chat/src/app/api/chat.py ' from __future__ import annotations from typing import List from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from app.db.session import get_db from app.core.security import get_current_user, UserClaims from app.schemas.chat import RoomCreate, RoomRead, MessageCreate, MessageRead from app.services.chat_service import ChatService router = APIRouter(prefix="/v1", tags=["chat"]) @router.post("/rooms", response_model=RoomRead, status_code=201) def create_room(payload: RoomCreate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): svc = ChatService(db) room = svc.create_room(title=payload.title, participant_ids=payload.participants, creator_id=user.sub) return room @router.get("/rooms", response_model=list[RoomRead]) def my_rooms(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): return ChatService(db).list_rooms_for_user(user.sub) @router.get("/rooms/{room_id}", response_model=RoomRead) def get_room(room_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): room = ChatService(db).get_room(room_id, user.sub) if not room: raise HTTPException(status_code=404, detail="Room not found") return room @router.post("/rooms/{room_id}/messages", response_model=MessageRead, status_code=201) def send_message(room_id: UUID, payload: MessageCreate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): svc = ChatService(db) room = svc.get_room(room_id, user.sub) if not room: raise HTTPException(status_code=404, detail="Room not found") msg = svc.create_message(room_id, user.sub, payload.content) return msg @router.get("/rooms/{room_id}/messages", response_model=list[MessageRead]) def list_messages( room_id: UUID, offset: int = 0, limit: int = Query(100, le=500), db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user), ): svc = ChatService(db) room = svc.get_room(room_id, user.sub) if not room: raise HTTPException(status_code=404, detail="Room not found") return svc.list_messages(room_id, user.sub, offset, limit) ' # ---------- match/src/app/api/routes/pairs.py ---------- write match/src/app/api/routes/pairs.py ' from __future__ import annotations from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from app.db.session import get_db from app.core.security import get_current_user, require_roles, UserClaims from app.schemas.pair import PairCreate, PairUpdate, PairRead from app.services.pair_service import PairService router = APIRouter(prefix="/v1/pairs", tags=["pairs"]) @router.post("", response_model=PairRead, status_code=201) def create_pair( payload: PairCreate, db: Session = Depends(get_db), user: UserClaims = Depends(require_roles("ADMIN","MATCHMAKER")), ): svc = PairService(db) return svc.create( user_id_a=payload.user_id_a, user_id_b=payload.user_id_b, score=payload.score, notes=payload.notes, created_by=user.sub, ) @router.get("", response_model=list[PairRead]) def list_pairs( for_user_id: str | None = None, status: str | None = None, offset: int = 0, limit: int = Query(50, le=200), db: Session = Depends(get_db), _: UserClaims = Depends(get_current_user), ): return PairService(db).list(for_user_id=for_user_id, status=status, offset=offset, limit=limit) @router.get("/{pair_id}", response_model=PairRead) def get_pair(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): pair = PairService(db).get(pair_id) if not pair: raise HTTPException(status_code=404, detail="Pair not found") return pair @router.patch("/{pair_id}", response_model=PairRead) def update_pair( pair_id: UUID, payload: PairUpdate, db: Session = Depends(get_db), user: UserClaims = Depends(require_roles("ADMIN")), ): updated = PairService(db).update(pair_id, payload) if not updated: raise HTTPException(status_code=404, detail="Pair not found") return updated @router.post("/{pair_id}/accept", response_model=PairRead) def accept(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): res = PairService(db).accept(pair_id, user.sub) if not res: raise HTTPException(status_code=404, detail="Pair not found") return res @router.post("/{pair_id}/reject", response_model=PairRead) def reject(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): res = PairService(db).reject(pair_id, user.sub) if not res: raise HTTPException(status_code=404, detail="Pair not found") return res @router.delete("/{pair_id}", status_code=204) def delete_pair( pair_id: UUID, db: Session = Depends(get_db), _: UserClaims = Depends(require_roles("ADMIN","MATCHMAKER")), ): svc = PairService(db) obj = svc.get(pair_id) if not obj: return svc.delete(obj) ' # ---------- profiles/src/app/api/routes/profiles.py ---------- write profiles/src/app/api/routes/profiles.py ' from __future__ import annotations from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Query, status from sqlalchemy.orm import Session import os from ...core.security import get_current_user, require_roles, UserClaims from ...db.session import get_db from ...models.profile import Profile from ...schemas.profile import ProfileCreate, ProfileUpdate, ProfileOut, LikesList from ...services.profile_service import ProfileService from ...services.profile_search_service import ProfileSearchService from ...services.likes_service import LikesService router = APIRouter(prefix="/v1/profiles", tags=["profiles"]) UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/app/uploads") BASE_EXTERNAL_URL = os.getenv("BASE_EXTERNAL_URL", "http://localhost:8080") @router.get("/me", response_model=ProfileOut) def get_me(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): prof = ProfileService(db).get_by_user_id(user.sub) if not prof: raise HTTPException(status_code=404, detail="Profile not found") return prof @router.post("", response_model=ProfileOut, status_code=201) def create_profile(payload: ProfileCreate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): svc = ProfileService(db) if svc.get_by_user_id(user.sub): raise HTTPException(status_code=409, detail="Profile already exists") return svc.create(user.sub, **payload.model_dump()) @router.get("", response_model=List[ProfileOut]) def list_profiles( q: Optional[str] = None, gender: Optional[str] = Query(None, pattern="^(male|female|other)$"), city: Optional[str] = None, languages: Optional[List[str]] = Query(None), interests: Optional[List[str]] = Query(None), has_photo: Optional[bool] = None, sort_by: Optional[str] = Query(None, pattern="^(created_at|updated_at|city|gender)$"), order: Optional[str] = Query("asc", pattern="^(asc|desc)$"), offset: int = 0, limit: int = Query(50, le=200), db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user), ): svc = ProfileSearchService(db) return svc.list_profiles( q=q, gender=gender, city=city, languages=languages, interests=interests, has_photo=has_photo, sort_by=sort_by, order=order, offset=offset, limit=limit, ) @router.get("/{profile_id}", response_model=ProfileOut) def get_profile(profile_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): prof = ProfileService(db).get(profile_id) if not prof: raise HTTPException(status_code=404, detail="Profile not found") return prof @router.get("/by-user/{user_id}", response_model=ProfileOut) def get_by_user(user_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): prof = ProfileService(db).get_by_user_id(user_id) if not prof: raise HTTPException(status_code=404, detail="Profile not found") return prof @router.patch("/me", response_model=ProfileOut) def patch_me(payload: ProfileUpdate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): svc = ProfileService(db) prof = svc.get_by_user_id(user.sub) if not prof: raise HTTPException(status_code=404, detail="Not found") data = payload.model_dump(exclude_none=True) return svc.update(prof, **data) @router.delete("/me", status_code=204) def delete_me(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): svc = ProfileService(db) prof = svc.get_by_user_id(user.sub) if not prof: return svc.delete(prof) return @router.post("/me/photo", response_model=ProfileOut) def upload_photo( file: UploadFile = File(...), db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user), ): svc = ProfileService(db) prof = svc.get_by_user_id(user.sub) if not prof: raise HTTPException(status_code=404, detail="Not found") if file.content_type not in ("image/jpeg", "image/png", "image/webp"): raise HTTPException(status_code=400, detail="Invalid content-type") os.makedirs(UPLOAD_DIR, exist_ok=True) ext = {"image/jpeg": "jpg", "image/png": "png", "image/webp": "webp"}[file.content_type] subdir = os.path.join(UPLOAD_DIR, "avatars") os.makedirs(subdir, exist_ok=True) filename = f"{user.sub}.{ext}" path = os.path.join(subdir, filename) with open(path, "wb") as f: f.write(file.file.read()) public_url = f"{BASE_EXTERNAL_URL}/profiles/static/avatars/{filename}" return svc.update(prof, photo_url=public_url) @router.delete("/me/photo", response_model=ProfileOut) def delete_photo(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): svc = ProfileService(db) prof = svc.get_by_user_id(user.sub) if not prof: raise HTTPException(status_code=404, detail="Not found") return svc.update(prof, photo_url=None) # Back-compat stub (hidden from schema) @router.get("/../likes", response_model=LikesList, include_in_schema=False) def _compat_likes_redirect(): return LikesList(items=[]) likes_router = APIRouter(prefix="/v1/likes", tags=["profiles"]) @likes_router.get("", response_model=LikesList) def my_likes(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): items = LikesService(db).list_my_likes(user.sub) return LikesList(items=items) @likes_router.put("/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT) def put_like(target_user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): LikesService(db).put_like(user.sub, target_user_id) return @likes_router.delete("/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_like(target_user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): LikesService(db).delete_like(user.sub, target_user_id) return @likes_router.get("/mutual", response_model=List[str]) def mutual(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)): return LikesService(db).mutual(user.sub) ' echo echo "Done. Backups saved as *.bak where files existed." echo "Rebuild & restart affected services, then re-run your audit:" echo " docker compose build chat match profiles && docker compose up -d" echo " ./scripts/audit.sh"