311 lines
12 KiB
Bash
Executable File
311 lines
12 KiB
Bash
Executable File
#!/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"
|