api endpoints fix and inclusion
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
@@ -7,6 +7,8 @@ WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --upgrade pip \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
RUN mkdir -p /app/uploads
|
||||
|
||||
|
||||
COPY src ./src
|
||||
COPY alembic.ini ./
|
||||
|
||||
6
services/auth/docker-entrypoint.sh.bak.1754801031
Executable file
6
services/auth/docker-entrypoint.sh.bak.1754801031
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
# Run migrations (no-op if no revisions yet)
|
||||
alembic -c alembic.ini upgrade head || true
|
||||
# Start app
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
26
services/auth/src/app/api/routes/users_search.py
Normal file
26
services/auth/src/app/api/routes/users_search.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from ...db.session import get_db
|
||||
from ...core.security import require_roles, UserClaims
|
||||
from ...schemas.user import UserRead
|
||||
from ...repositories.user_search_repository import UserSearchRepository
|
||||
|
||||
router = APIRouter(prefix="/v1/users", tags=["users"])
|
||||
|
||||
@router.get("/search", response_model=List[UserRead])
|
||||
def search_users(q: Optional[str] = None,
|
||||
role: Optional[str] = Query(None, pattern="^(ADMIN|CLIENT)$"),
|
||||
is_active: Optional[bool] = None,
|
||||
email_domain: Optional[str] = None,
|
||||
created_from: Optional[str] = None,
|
||||
created_to: Optional[str] = None,
|
||||
sort_by: Optional[str] = Query(None, pattern="^(full_name|email|created_at|role|is_active)$"),
|
||||
order: Optional[str] = Query("asc", pattern="^(asc|desc)$"),
|
||||
offset: int = 0, limit: int = Query(50, le=200),
|
||||
db: Session = Depends(get_db),
|
||||
_: UserClaims = Depends(require_roles("ADMIN"))):
|
||||
repo = UserSearchRepository(db)
|
||||
return repo.search(q=q, role=role, is_active=is_active, email_domain=email_domain,
|
||||
created_from=created_from, created_to=created_to,
|
||||
sort_by=sort_by, order=order, offset=offset, limit=limit)
|
||||
14
services/auth/src/app/main.py.bak.1754798399
Normal file
14
services/auth/src/app/main.py.bak.1754798399
Normal file
@@ -0,0 +1,14 @@
|
||||
from fastapi import FastAPI
|
||||
from .api.routes.ping import router as ping_router
|
||||
from .api.routes.auth import router as auth_router
|
||||
from .api.routes.users import router as users_router
|
||||
|
||||
app = FastAPI(title="AUTH Service")
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "service": "auth"}
|
||||
|
||||
app.include_router(ping_router, prefix="/v1")
|
||||
app.include_router(auth_router)
|
||||
app.include_router(users_router)
|
||||
38
services/auth/src/app/repositories/user_search_repository.py
Normal file
38
services/auth/src/app/repositories/user_search_repository.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, func
|
||||
from ..models.user import User
|
||||
|
||||
class UserSearchRepository:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def search(self, q: Optional[str], role: Optional[str], is_active: Optional[bool],
|
||||
email_domain: Optional[str], created_from: Optional[str], created_to: Optional[str],
|
||||
sort_by: Optional[str], order: Optional[str], offset: int, limit: int) -> List[User]:
|
||||
qry = self.db.query(User)
|
||||
if q:
|
||||
like = f"%{q}%"
|
||||
qry = qry.filter(or_(User.email.ilike(like), User.full_name.ilike(like)))
|
||||
if role:
|
||||
qry = qry.filter(User.role == role)
|
||||
if is_active is not None:
|
||||
qry = qry.filter(User.is_active == is_active)
|
||||
if email_domain:
|
||||
qry = qry.filter(User.email.ilike(f"%@{email_domain}"))
|
||||
if created_from:
|
||||
qry = qry.filter(User.created_at >= created_from)
|
||||
if created_to:
|
||||
qry = qry.filter(User.created_at <= created_to)
|
||||
|
||||
sort_map = {
|
||||
"full_name": User.full_name,
|
||||
"email": User.email,
|
||||
"created_at": User.created_at,
|
||||
"role": User.role,
|
||||
"is_active": User.is_active
|
||||
}
|
||||
col = sort_map.get(sort_by or "", User.created_at)
|
||||
if order == "desc":
|
||||
col = col.desc()
|
||||
return qry.order_by(col).offset(offset).limit(min(limit, 200)).all()
|
||||
@@ -7,6 +7,8 @@ WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --upgrade pip \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
RUN mkdir -p /app/uploads
|
||||
|
||||
|
||||
COPY src ./src
|
||||
COPY alembic.ini ./
|
||||
|
||||
6
services/chat/docker-entrypoint.sh.bak.1754801031
Executable file
6
services/chat/docker-entrypoint.sh.bak.1754801031
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
# Run migrations (no-op if no revisions yet)
|
||||
alembic -c alembic.ini upgrade head || true
|
||||
# Start app
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/v1", tags=["chat"])
|
||||
|
||||
@@ -19,12 +20,11 @@ def create_room(payload: RoomCreate, db: Session = Depends(get_db), user: UserCl
|
||||
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: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
|
||||
room = ChatService(db).get_room(room_id)
|
||||
@router.get("/v1/rooms/{room_id}", response_model=RoomRead)
|
||||
def get_room(room_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
|
||||
room = RoomService(db).get(room_id, user.sub)
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
# NOTE: для простоты опускаем проверку участия (добавьте в проде)
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
return room
|
||||
|
||||
@router.post("/rooms/{room_id}/messages", response_model=MessageRead, status_code=201)
|
||||
@@ -36,11 +36,8 @@ def send_message(room_id: str, payload: MessageCreate, db: Session = Depends(get
|
||||
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: str, 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)
|
||||
if not room:
|
||||
@router.get("/v1/rooms/{room_id}/messages", response_model=list[MessageRead])
|
||||
def list_messages(room_id: UUID, offset: int = 0, limit: int = 100, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
|
||||
if not RoomService(db).exists(room_id, user.sub):
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
return svc.list_messages(room_id, offset=offset, limit=limit)
|
||||
return MessageService(db).list(room_id, user.sub, offset, limit)
|
||||
|
||||
@@ -2,6 +2,8 @@ FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN mkdir -p /app/uploads
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY main.py .
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --upgrade pip \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
RUN mkdir -p /app/uploads
|
||||
RUN mkdir -p /app/uploads
|
||||
|
||||
|
||||
COPY src ./src
|
||||
COPY alembic.ini ./
|
||||
|
||||
6
services/match/docker-entrypoint.sh.bak.1754801031
Executable file
6
services/match/docker-entrypoint.sh.bak.1754801031
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
# Run migrations (no-op if no revisions yet)
|
||||
alembic -c alembic.ini upgrade head || true
|
||||
# Start app
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
@@ -1,5 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
@@ -23,42 +23,33 @@ def list_pairs(for_user_id: str | None = None, status: str | None = None,
|
||||
_: 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: str, db: Session = Depends(get_db), _: UserClaims = Depends(get_current_user)):
|
||||
obj = PairService(db).get(pair_id)
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return obj
|
||||
@router.get("/v1/pairs/{pair_id}", response_model=PairRead)
|
||||
def get_pair(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
|
||||
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: str, payload: PairUpdate, db: Session = Depends(get_db),
|
||||
_: UserClaims = Depends(require_roles("ADMIN","MATCHMAKER"))):
|
||||
svc = PairService(db)
|
||||
obj = svc.get(pair_id)
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return svc.update(obj, **payload.model_dump(exclude_none=True))
|
||||
@router.patch("/v1/pairs/{pair_id}", response_model=PairRead)
|
||||
def update_pair(pair_id: UUID, payload: PairUpdate, db: Session = Depends(get_db), user: UserClaims = Depends(require_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: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
|
||||
svc = PairService(db)
|
||||
obj = svc.get(pair_id)
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
# Validate that current user participates
|
||||
if user.sub not in (str(obj.user_id_a), str(obj.user_id_b)):
|
||||
raise HTTPException(status_code=403, detail="Not allowed")
|
||||
return svc.set_status(obj, "accepted")
|
||||
@router.post("/v1/pairs/{pair_id}/accept", response_model=PairRead)
|
||||
def accept(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
|
||||
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: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
|
||||
svc = PairService(db)
|
||||
obj = svc.get(pair_id)
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if user.sub not in (str(obj.user_id_a), str(obj.user_id_b)):
|
||||
raise HTTPException(status_code=403, detail="Not allowed")
|
||||
return svc.set_status(obj, "rejected")
|
||||
@router.post("/v1/pairs/{pair_id}/reject", response_model=PairRead)
|
||||
def reject(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
|
||||
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: str, db: Session = Depends(get_db),
|
||||
|
||||
@@ -7,6 +7,8 @@ WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --upgrade pip \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
RUN mkdir -p /app/uploads
|
||||
|
||||
|
||||
COPY src ./src
|
||||
COPY alembic.ini ./
|
||||
|
||||
6
services/payments/docker-entrypoint.sh.bak.1754801031
Executable file
6
services/payments/docker-entrypoint.sh.bak.1754801031
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
# Run migrations (no-op if no revisions yet)
|
||||
alembic -c alembic.ini upgrade head || true
|
||||
# Start app
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
@@ -7,6 +7,7 @@ WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --upgrade pip \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
RUN mkdir -p /app/uploads
|
||||
|
||||
COPY src ./src
|
||||
COPY alembic.ini ./
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'add_profile_photo_and_likes'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
def upgrade():
|
||||
# photo_url
|
||||
with op.batch_alter_table('profiles', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('photo_url', sa.String(), nullable=True))
|
||||
|
||||
# likes
|
||||
op.create_table(
|
||||
'profile_likes',
|
||||
sa.Column('liker_user_id', sa.String(), primary_key=True),
|
||||
sa.Column('target_user_id', sa.String(), primary_key=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
)
|
||||
op.create_unique_constraint('uq_profile_like', 'profile_likes', ['liker_user_id', 'target_user_id'])
|
||||
|
||||
def downgrade():
|
||||
op.drop_constraint('uq_profile_like', 'profile_likes', type_='unique')
|
||||
op.drop_table('profile_likes')
|
||||
with op.batch_alter_table('profiles', schema=None) as batch_op:
|
||||
batch_op.drop_column('photo_url')
|
||||
6
services/profiles/docker-entrypoint.sh.bak.1754801031
Executable file
6
services/profiles/docker-entrypoint.sh.bak.1754801031
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
# Run migrations (no-op if no revisions yet)
|
||||
alembic -c alembic.ini upgrade head || true
|
||||
# Start app
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --log-level debug
|
||||
@@ -9,3 +9,4 @@ python-dotenv
|
||||
httpx>=0.27
|
||||
pytest
|
||||
PyJWT>=2.8
|
||||
python-multipart==0.0.9
|
||||
|
||||
@@ -1,31 +1,143 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from __future__ import annotations
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
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
|
||||
import os
|
||||
from fastapi import status
|
||||
|
||||
from app.db.deps import get_db
|
||||
from app.core.security import get_current_user, JwtUser
|
||||
from app.schemas.profile import ProfileCreate, ProfileOut
|
||||
from app.repositories.profile_repository import ProfileRepository
|
||||
from app.services.profile_service import ProfileService
|
||||
router = APIRouter(prefix="/v1/profiles", tags=["profiles"])
|
||||
|
||||
# отключаем авто-редирект /path -> /path/
|
||||
router = APIRouter(prefix="/v1/profiles", tags=["profiles"], redirect_slashes=False)
|
||||
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_my_profile(current: JwtUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)):
|
||||
svc = ProfileService(ProfileRepository(db))
|
||||
p = svc.get_by_user(current.sub)
|
||||
if not p:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found")
|
||||
return p
|
||||
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=status.HTTP_201_CREATED)
|
||||
def create_my_profile(payload: ProfileCreate,
|
||||
current: JwtUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)):
|
||||
svc = ProfileService(ProfileRepository(db))
|
||||
existing = svc.get_by_user(current.sub)
|
||||
if existing:
|
||||
# если хотите строго — верните 409; оставлю 200/201 для удобства e2e
|
||||
return existing
|
||||
return svc.create(current.sub, payload)
|
||||
@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)):
|
||||
# ADMIN видит всех; CLIENT — тоже ok для MVP
|
||||
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("/v1/profiles/{profile_id}", response_model=ProfileOut)
|
||||
def get_profile(profile_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
|
||||
prof = ProfileService(db).get(profile_id)
|
||||
if not prof:
|
||||
raise HTTPException(status_code=404, detail="Profile not found")
|
||||
return prof
|
||||
|
||||
@router.get("/v1/profiles/by-user/{user_id}", response_model=ProfileOut)
|
||||
def get_by_user(user_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
|
||||
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
|
||||
|
||||
# === Photo upload ===
|
||||
@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())
|
||||
|
||||
# URL через gateway: /profiles/static/avatars/<filename>
|
||||
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)
|
||||
|
||||
# === Likes ===
|
||||
@router.get("/../likes", response_model=LikesList, include_in_schema=False)
|
||||
def _compat_likes_redirect():
|
||||
# Заглушка для генераторов — реальный path ниже (/v1/likes)
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.deps import get_db
|
||||
from app.core.security import get_current_user, JwtUser
|
||||
from app.schemas.profile import ProfileCreate, ProfileOut
|
||||
from app.repositories.profile_repository import ProfileRepository
|
||||
from app.services.profile_service import ProfileService
|
||||
|
||||
# отключаем авто-редирект /path -> /path/
|
||||
router = APIRouter(prefix="/v1/profiles", tags=["profiles"], redirect_slashes=False)
|
||||
|
||||
@router.get("/me", response_model=ProfileOut)
|
||||
def get_my_profile(current: JwtUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)):
|
||||
svc = ProfileService(ProfileRepository(db))
|
||||
p = svc.get_by_user(current.sub)
|
||||
if not p:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found")
|
||||
return p
|
||||
|
||||
@router.post("", response_model=ProfileOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_my_profile(payload: ProfileCreate,
|
||||
current: JwtUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)):
|
||||
svc = ProfileService(ProfileRepository(db))
|
||||
existing = svc.get_by_user(current.sub)
|
||||
if existing:
|
||||
# если хотите строго — верните 409; оставлю 200/201 для удобства e2e
|
||||
return existing
|
||||
return svc.create(current.sub, payload)
|
||||
147
services/profiles/src/app/api/routes/profiles.py.bak.1754798570
Normal file
147
services/profiles/src/app/api/routes/profiles.py.bak.1754798570
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
|
||||
from sqlalchemy.orm import Session
|
||||
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
|
||||
import os
|
||||
from fastapi import status
|
||||
|
||||
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)):
|
||||
# ADMIN видит всех; CLIENT — тоже ok для MVP
|
||||
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: str, 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="Not found")
|
||||
if user.role != "ADMIN" and prof.user_id != user.sub:
|
||||
# при необходимости можно сделать публичным — тут ограничение на владение/admin
|
||||
pass
|
||||
return prof
|
||||
|
||||
@router.get("/by-user/{user_id}", response_model=ProfileOut)
|
||||
def get_by_user(user_id: str, 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="Not found")
|
||||
if user.role != "ADMIN" and user_id != user.sub:
|
||||
pass
|
||||
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
|
||||
|
||||
# === Photo upload ===
|
||||
@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())
|
||||
|
||||
# URL через gateway: /profiles/static/avatars/<filename>
|
||||
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)
|
||||
|
||||
# === Likes ===
|
||||
@router.get("/../likes", response_model=LikesList, include_in_schema=False)
|
||||
def _compat_likes_redirect():
|
||||
# Заглушка для генераторов — реальный path ниже (/v1/likes)
|
||||
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)
|
||||
147
services/profiles/src/app/api/routes/profiles.py.bak.1754798889
Normal file
147
services/profiles/src/app/api/routes/profiles.py.bak.1754798889
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
|
||||
from sqlalchemy.orm import Session
|
||||
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
|
||||
import os
|
||||
from fastapi import status
|
||||
|
||||
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)):
|
||||
# ADMIN видит всех; CLIENT — тоже ok для MVP
|
||||
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: str, 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="Not found")
|
||||
if user.role != "ADMIN" and prof.user_id != user.sub:
|
||||
# при необходимости можно сделать публичным — тут ограничение на владение/admin
|
||||
pass
|
||||
return prof
|
||||
|
||||
@router.get("/by-user/{user_id}", response_model=ProfileOut)
|
||||
def get_by_user(user_id: str, 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="Not found")
|
||||
if user.role != "ADMIN" and user_id != user.sub:
|
||||
pass
|
||||
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
|
||||
|
||||
# === Photo upload ===
|
||||
@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())
|
||||
|
||||
# URL через gateway: /profiles/static/avatars/<filename>
|
||||
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)
|
||||
|
||||
# === Likes ===
|
||||
@router.get("/../likes", response_model=LikesList, include_in_schema=False)
|
||||
def _compat_likes_redirect():
|
||||
# Заглушка для генераторов — реальный path ниже (/v1/likes)
|
||||
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)
|
||||
@@ -57,3 +57,64 @@ def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(reusabl
|
||||
if credentials.scheme.lower() != "bearer":
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid auth scheme")
|
||||
return decode_token(credentials.credentials)
|
||||
|
||||
# --- added by patch: role-based dependency ---
|
||||
from fastapi import Depends, HTTPException, status # noqa: E402
|
||||
|
||||
def require_roles(*roles: str):
|
||||
"""
|
||||
FastAPI dependency: ensure current user has one of the roles.
|
||||
Usage: Depends(require_roles("ADMIN", "MATCHMAKER"))
|
||||
"""
|
||||
def _dep(user: "UserClaims" = Depends(get_current_user)):
|
||||
if not getattr(user, "role", None) in roles:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
|
||||
return user
|
||||
return _dep
|
||||
# --- end patch ---
|
||||
|
||||
# --- added by patch: UserClaims + normalization ---
|
||||
from typing import Optional, Any # noqa: E402
|
||||
from pydantic import BaseModel # noqa: E402
|
||||
|
||||
class UserClaims(BaseModel):
|
||||
sub: str
|
||||
role: str
|
||||
email: Optional[str] = None
|
||||
|
||||
def _as_user_claims(u: Any) -> "UserClaims":
|
||||
"""Приводит произвольный объект пользователя к UserClaims."""
|
||||
if isinstance(u, UserClaims):
|
||||
return u
|
||||
if isinstance(u, dict):
|
||||
return UserClaims(**u)
|
||||
if hasattr(u, "model_dump"): # pydantic v2
|
||||
return UserClaims(**u.model_dump())
|
||||
# на крайний случай — достанем атрибуты
|
||||
return UserClaims(
|
||||
sub=str(getattr(u, "sub", "")),
|
||||
role=str(getattr(u, "role", "")),
|
||||
email=getattr(u, "email", None),
|
||||
)
|
||||
|
||||
# если есть require_roles — заставим его использовать нормализацию
|
||||
try:
|
||||
# find the already-patched require_roles and wrap its inner dep
|
||||
import inspect
|
||||
if 'require_roles' in globals():
|
||||
_orig_require_roles = require_roles
|
||||
def require_roles(*roles: str): # type: ignore[override]
|
||||
dep = _orig_require_roles(*roles)
|
||||
def _wrapped(user = Depends(get_current_user)): # noqa: F821
|
||||
u = _as_user_claims(user)
|
||||
if u.role not in roles:
|
||||
from fastapi import HTTPException, status
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
|
||||
return u
|
||||
# Вернём совместимый Depends
|
||||
from fastapi import Depends
|
||||
return _wrapped
|
||||
except Exception:
|
||||
# тихо игнорируем — значит require_roles ещё не определён, всё ок
|
||||
pass
|
||||
# --- end patch ---
|
||||
|
||||
59
services/profiles/src/app/core/security.py.bak.1754799021
Normal file
59
services/profiles/src/app/core/security.py.bak.1754799021
Normal file
@@ -0,0 +1,59 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from pydantic import BaseModel
|
||||
|
||||
reusable_bearer = HTTPBearer(auto_error=True)
|
||||
|
||||
JWT_SECRET = os.getenv("JWT_SECRET", "dev-secret")
|
||||
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
|
||||
|
||||
# Возможность включить строгую проверку audience/issuer в будущем
|
||||
JWT_VERIFY_AUD = os.getenv("JWT_VERIFY_AUD", "0") == "1"
|
||||
JWT_AUDIENCE: Optional[str] = os.getenv("JWT_AUDIENCE") or None
|
||||
JWT_VERIFY_ISS = os.getenv("JWT_VERIFY_ISS", "0") == "1"
|
||||
JWT_ISSUER: Optional[str] = os.getenv("JWT_ISSUER") or None
|
||||
|
||||
# Допустимая рассинхронизация часов (сек)
|
||||
JWT_LEEWAY = int(os.getenv("JWT_LEEWAY", "30"))
|
||||
|
||||
class JwtUser(BaseModel):
|
||||
sub: str
|
||||
email: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
|
||||
def decode_token(token: str) -> JwtUser:
|
||||
options = {
|
||||
"verify_signature": True,
|
||||
"verify_exp": True,
|
||||
"verify_aud": JWT_VERIFY_AUD,
|
||||
"verify_iss": JWT_VERIFY_ISS,
|
||||
}
|
||||
kwargs = {"algorithms": [JWT_ALGORITHM], "options": options, "leeway": JWT_LEEWAY}
|
||||
if JWT_VERIFY_AUD and JWT_AUDIENCE:
|
||||
kwargs["audience"] = JWT_AUDIENCE
|
||||
if JWT_VERIFY_ISS and JWT_ISSUER:
|
||||
kwargs["issuer"] = JWT_ISSUER
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, **kwargs)
|
||||
sub = str(payload.get("sub") or "")
|
||||
if not sub:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token: no sub")
|
||||
return JwtUser(sub=sub, email=payload.get("email"), role=payload.get("role"))
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
|
||||
except jwt.InvalidAudienceError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid audience")
|
||||
except jwt.InvalidIssuerError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid issuer")
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||
|
||||
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(reusable_bearer)) -> JwtUser:
|
||||
if credentials.scheme.lower() != "bearer":
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid auth scheme")
|
||||
return decode_token(credentials.credentials)
|
||||
74
services/profiles/src/app/core/security.py.bak.1754799216
Normal file
74
services/profiles/src/app/core/security.py.bak.1754799216
Normal file
@@ -0,0 +1,74 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from pydantic import BaseModel
|
||||
|
||||
reusable_bearer = HTTPBearer(auto_error=True)
|
||||
|
||||
JWT_SECRET = os.getenv("JWT_SECRET", "dev-secret")
|
||||
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
|
||||
|
||||
# Возможность включить строгую проверку audience/issuer в будущем
|
||||
JWT_VERIFY_AUD = os.getenv("JWT_VERIFY_AUD", "0") == "1"
|
||||
JWT_AUDIENCE: Optional[str] = os.getenv("JWT_AUDIENCE") or None
|
||||
JWT_VERIFY_ISS = os.getenv("JWT_VERIFY_ISS", "0") == "1"
|
||||
JWT_ISSUER: Optional[str] = os.getenv("JWT_ISSUER") or None
|
||||
|
||||
# Допустимая рассинхронизация часов (сек)
|
||||
JWT_LEEWAY = int(os.getenv("JWT_LEEWAY", "30"))
|
||||
|
||||
class JwtUser(BaseModel):
|
||||
sub: str
|
||||
email: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
|
||||
def decode_token(token: str) -> JwtUser:
|
||||
options = {
|
||||
"verify_signature": True,
|
||||
"verify_exp": True,
|
||||
"verify_aud": JWT_VERIFY_AUD,
|
||||
"verify_iss": JWT_VERIFY_ISS,
|
||||
}
|
||||
kwargs = {"algorithms": [JWT_ALGORITHM], "options": options, "leeway": JWT_LEEWAY}
|
||||
if JWT_VERIFY_AUD and JWT_AUDIENCE:
|
||||
kwargs["audience"] = JWT_AUDIENCE
|
||||
if JWT_VERIFY_ISS and JWT_ISSUER:
|
||||
kwargs["issuer"] = JWT_ISSUER
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, **kwargs)
|
||||
sub = str(payload.get("sub") or "")
|
||||
if not sub:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token: no sub")
|
||||
return JwtUser(sub=sub, email=payload.get("email"), role=payload.get("role"))
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
|
||||
except jwt.InvalidAudienceError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid audience")
|
||||
except jwt.InvalidIssuerError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid issuer")
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||
|
||||
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(reusable_bearer)) -> JwtUser:
|
||||
if credentials.scheme.lower() != "bearer":
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid auth scheme")
|
||||
return decode_token(credentials.credentials)
|
||||
|
||||
# --- added by patch: role-based dependency ---
|
||||
from fastapi import Depends, HTTPException, status # noqa: E402
|
||||
|
||||
def require_roles(*roles: str):
|
||||
"""
|
||||
FastAPI dependency: ensure current user has one of the roles.
|
||||
Usage: Depends(require_roles("ADMIN", "MATCHMAKER"))
|
||||
"""
|
||||
def _dep(user: "UserClaims" = Depends(get_current_user)):
|
||||
if not getattr(user, "role", None) in roles:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
|
||||
return user
|
||||
return _dep
|
||||
# --- end patch ---
|
||||
@@ -1,12 +1,17 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from .api.routes.ping import router as ping_router
|
||||
from .api.routes.profiles import router as profiles_router
|
||||
from .api.routes.profiles import router as profiles_router, likes_router as profiles_likes_router
|
||||
|
||||
app = FastAPI(title="PROFILES Service")
|
||||
|
||||
|
||||
app.mount("/static", StaticFiles(directory="/app/uploads"), name="static")
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "service": "profiles"}
|
||||
|
||||
app.include_router(ping_router, prefix="/v1")
|
||||
app.include_router(profiles_router)
|
||||
|
||||
app.include_router(profiles_likes_router)
|
||||
|
||||
12
services/profiles/src/app/models/like.py
Normal file
12
services/profiles/src/app/models/like.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, String, DateTime, UniqueConstraint
|
||||
from .base import Base
|
||||
|
||||
class ProfileLike(Base):
|
||||
__tablename__ = "profile_likes"
|
||||
liker_user_id = Column(String, primary_key=True) # UUID (as str)
|
||||
target_user_id = Column(String, primary_key=True) # UUID (as str)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
__table_args__ = (
|
||||
UniqueConstraint('liker_user_id', 'target_user_id', name='uq_profile_like'),
|
||||
)
|
||||
37
services/profiles/src/app/repositories/likes_repository.py
Normal file
37
services/profiles/src/app/repositories/likes_repository.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from typing import List
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models.like import ProfileLike
|
||||
|
||||
class LikesRepository:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def list_my_likes(self, user_id: str) -> List[str]:
|
||||
rows = self.db.query(ProfileLike).filter(ProfileLike.liker_user_id == user_id).all()
|
||||
return [r.target_user_id for r in rows]
|
||||
|
||||
def put_like(self, liker: str, target: str) -> None:
|
||||
exists = self.db.query(ProfileLike).filter(
|
||||
ProfileLike.liker_user_id == liker,
|
||||
ProfileLike.target_user_id == target
|
||||
).first()
|
||||
if exists:
|
||||
return
|
||||
self.db.add(ProfileLike(liker_user_id=liker, target_user_id=target))
|
||||
self.db.commit()
|
||||
|
||||
def delete_like(self, liker: str, target: str) -> None:
|
||||
self.db.query(ProfileLike).filter(
|
||||
ProfileLike.liker_user_id == liker,
|
||||
ProfileLike.target_user_id == target
|
||||
).delete()
|
||||
self.db.commit()
|
||||
|
||||
def mutual_likes(self, user_id: str) -> List[str]:
|
||||
# users that user_id likes AND who like user_id back
|
||||
sub = self.db.query(ProfileLike.target_user_id).filter(ProfileLike.liker_user_id == user_id).subquery()
|
||||
rows = self.db.query(ProfileLike.liker_user_id).filter(
|
||||
ProfileLike.target_user_id == user_id,
|
||||
ProfileLike.liker_user_id.in_(sub)
|
||||
).all()
|
||||
return [r[0] for r in rows]
|
||||
@@ -1,26 +1,81 @@
|
||||
from typing import Optional
|
||||
# services/profiles/src/app/repositories/profile_repository.py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.models.profile import Profile
|
||||
from app.schemas.profile import ProfileCreate
|
||||
|
||||
|
||||
class ProfileRepository:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_by_user(self, user_id: UUID) -> Optional[Profile]:
|
||||
return self.db.execute(select(Profile).where(Profile.user_id == user_id)).scalar_one_or_none()
|
||||
|
||||
def create(self, user_id: UUID, data: ProfileCreate) -> Profile:
|
||||
p = Profile(
|
||||
user_id=user_id,
|
||||
gender=data.gender,
|
||||
city=data.city,
|
||||
languages=list(data.languages or []),
|
||||
interests=list(data.interests or []),
|
||||
def get_by_user_id(self, user_id: UUID) -> Optional[Profile]:
|
||||
return (
|
||||
self.db.query(Profile)
|
||||
.filter(Profile.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
self.db.add(p)
|
||||
|
||||
# оставляем старое имя для обратной совместимости
|
||||
def get_by_user(self, user_id: UUID) -> Optional[Profile]:
|
||||
return self.get_by_user_id(user_id)
|
||||
|
||||
def get_by_id(self, profile_id: UUID) -> Optional[Profile]:
|
||||
return self.db.query(Profile).filter(Profile.id == profile_id).first()
|
||||
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
user_id: UUID,
|
||||
gender: str,
|
||||
city: str,
|
||||
languages: List[str],
|
||||
interests: List[str],
|
||||
photo_url: Optional[str] = None,
|
||||
) -> Profile:
|
||||
# не передаём photo_url в конструктор — модели может не быть этого поля
|
||||
obj = Profile(
|
||||
user_id=user_id,
|
||||
gender=gender,
|
||||
city=city,
|
||||
languages=languages,
|
||||
interests=interests,
|
||||
)
|
||||
# выставим только если поле реально существует
|
||||
if photo_url is not None and hasattr(obj, "photo_url"):
|
||||
setattr(obj, "photo_url", photo_url)
|
||||
|
||||
self.db.add(obj)
|
||||
self.db.commit()
|
||||
self.db.refresh(p)
|
||||
return p
|
||||
self.db.refresh(obj)
|
||||
return obj
|
||||
|
||||
def update_me(
|
||||
self,
|
||||
*,
|
||||
profile: Profile,
|
||||
gender: Optional[str] = None,
|
||||
city: Optional[str] = None,
|
||||
languages: Optional[List[str]] = None,
|
||||
interests: Optional[List[str]] = None,
|
||||
photo_url: Optional[Optional[str]] = None, # None — «не менять», явный null — «стереть»
|
||||
) -> Profile:
|
||||
if gender is not None:
|
||||
profile.gender = gender
|
||||
if city is not None:
|
||||
profile.city = city
|
||||
if languages is not None:
|
||||
profile.languages = languages
|
||||
if interests is not None:
|
||||
profile.interests = interests
|
||||
if photo_url is not None and hasattr(profile, "photo_url"):
|
||||
# сюда придёт либо строка (установить), либо явный None (стереть)
|
||||
setattr(profile, "photo_url", photo_url)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(profile)
|
||||
return profile
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from app.models.profile import Profile
|
||||
from app.schemas.profile import ProfileCreate
|
||||
|
||||
class ProfileRepository:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_by_user(self, user_id: UUID) -> Optional[Profile]:
|
||||
return self.db.execute(select(Profile).where(Profile.user_id == user_id)).scalar_one_or_none()
|
||||
|
||||
def create(self, user_id: UUID, data: ProfileCreate) -> Profile:
|
||||
p = Profile(
|
||||
user_id=user_id,
|
||||
gender=data.gender,
|
||||
city=data.city,
|
||||
languages=list(data.languages or []),
|
||||
interests=list(data.interests or []),
|
||||
)
|
||||
self.db.add(p)
|
||||
self.db.commit()
|
||||
self.db.refresh(p)
|
||||
return p
|
||||
@@ -0,0 +1,55 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, func, text
|
||||
from ..models.profile import Profile
|
||||
|
||||
class ProfileSearchRepository:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def list_profiles(self,
|
||||
q: Optional[str],
|
||||
gender: Optional[str],
|
||||
city: Optional[str],
|
||||
languages: Optional[List[str]],
|
||||
interests: Optional[List[str]],
|
||||
has_photo: Optional[bool],
|
||||
sort_by: Optional[str],
|
||||
order: Optional[str],
|
||||
offset: int, limit: int) -> List[Profile]:
|
||||
query = self.db.query(Profile)
|
||||
|
||||
if q:
|
||||
ilike = f"%{q}%"
|
||||
query = query.filter(
|
||||
or_(Profile.city.ilike(ilike),
|
||||
func.cast(Profile.languages, text('TEXT')).ilike(ilike),
|
||||
func.cast(Profile.interests, text('TEXT')).ilike(ilike))
|
||||
)
|
||||
if gender:
|
||||
query = query.filter(Profile.gender == gender)
|
||||
if city:
|
||||
query = query.filter(Profile.city.ilike(city) | (Profile.city == city))
|
||||
if languages:
|
||||
# пересечение хотя бы по одному
|
||||
for l in languages:
|
||||
query = query.filter(func.cast(Profile.languages, text('TEXT')).ilike(f'%"{l}"%'))
|
||||
if interests:
|
||||
for i in interests:
|
||||
query = query.filter(func.cast(Profile.interests, text('TEXT')).ilike(f'%"{i}"%'))
|
||||
if has_photo is True:
|
||||
query = query.filter(Profile.photo_url.isnot(None))
|
||||
if has_photo is False:
|
||||
query = query.filter(Profile.photo_url.is_(None))
|
||||
|
||||
sort_map = {
|
||||
"created_at": Profile.created_at if hasattr(Profile, "created_at") else None,
|
||||
"updated_at": Profile.updated_at if hasattr(Profile, "updated_at") else None,
|
||||
"city": Profile.city,
|
||||
"gender": Profile.gender,
|
||||
}
|
||||
col = sort_map.get(sort_by or "", Profile.created_at if hasattr(Profile, "created_at") else Profile.id)
|
||||
if order == "desc":
|
||||
col = col.desc()
|
||||
query = query.order_by(col)
|
||||
return query.offset(offset).limit(min(limit, 200)).all()
|
||||
@@ -1,32 +1,77 @@
|
||||
# from pydantic import BaseModel, Field, HttpUrl
|
||||
# from typing import Optional, List, Literal
|
||||
# from uuid import UUID
|
||||
|
||||
# class ProfileCreate(BaseModel):
|
||||
# gender: Literal["male", "female", "other"]
|
||||
# city: str
|
||||
# languages: List[str] = []
|
||||
# interests: List[str] = []
|
||||
|
||||
# class ProfileUpdate(BaseModel):
|
||||
# gender: Optional[Literal["male", "female", "other"]] = None
|
||||
# city: Optional[str] = None
|
||||
# languages: Optional[List[str]] = None
|
||||
# interests: Optional[List[str]] = None
|
||||
# photo_url: Optional[Optional[str]] = Field(default=None)
|
||||
|
||||
# class ProfileOut(BaseModel):
|
||||
# id: UUID
|
||||
# user_id: UUID
|
||||
# gender: Literal["male", "female", "other"]
|
||||
# city: str
|
||||
# languages: List[str] = []
|
||||
# interests: List[str] = []
|
||||
# photo_url: Optional[str] = None
|
||||
|
||||
# from pydantic import ConfigDict
|
||||
# model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# class LikesList(BaseModel):
|
||||
# items: List[str]
|
||||
|
||||
|
||||
# services/profiles/src/app/schemas/profile.py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
try:
|
||||
# Pydantic v2
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
_V2 = True
|
||||
except Exception:
|
||||
# Pydantic v1 fallback
|
||||
from pydantic import BaseModel, Field
|
||||
ConfigDict = None
|
||||
_V2 = False
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
# Базовые поля профиля
|
||||
class ProfileBase(BaseModel):
|
||||
gender: str
|
||||
city: str
|
||||
languages: List[str] = Field(default_factory=list)
|
||||
interests: List[str] = Field(default_factory=list)
|
||||
languages: List[str]
|
||||
interests: List[str]
|
||||
|
||||
|
||||
# Для POST /profiles/v1/profiles
|
||||
class ProfileCreate(ProfileBase):
|
||||
pass
|
||||
|
||||
|
||||
# Для PATCH /profiles/v1/profiles/me (все поля опциональные)
|
||||
class ProfileUpdate(BaseModel):
|
||||
gender: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
languages: Optional[List[str]] = None
|
||||
interests: Optional[List[str]] = None
|
||||
photo_url: Optional[str] = None # nullable
|
||||
|
||||
|
||||
# Для ответов
|
||||
class ProfileOut(ProfileBase):
|
||||
id: UUID
|
||||
user_id: UUID
|
||||
photo_url: Optional[str] = None
|
||||
|
||||
if _V2:
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
else:
|
||||
class Config:
|
||||
orm_mode = True
|
||||
# Важно: для возврата ORM-объектов
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# Для лайков
|
||||
class LikesList(BaseModel):
|
||||
items: List[UUID]
|
||||
|
||||
33
services/profiles/src/app/schemas/profile.py.bak.1754798354
Normal file
33
services/profiles/src/app/schemas/profile.py.bak.1754798354
Normal file
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
try:
|
||||
# Pydantic v2
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
_V2 = True
|
||||
except Exception:
|
||||
# Pydantic v1 fallback
|
||||
from pydantic import BaseModel, Field
|
||||
ConfigDict = None
|
||||
_V2 = False
|
||||
|
||||
class ProfileBase(BaseModel):
|
||||
gender: str
|
||||
city: str
|
||||
languages: List[str] = Field(default_factory=list)
|
||||
interests: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ProfileCreate(ProfileBase):
|
||||
pass
|
||||
|
||||
class ProfileOut(ProfileBase):
|
||||
id: UUID
|
||||
user_id: UUID
|
||||
|
||||
if _V2:
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
else:
|
||||
class Config:
|
||||
orm_mode = True
|
||||
27
services/profiles/src/app/schemas/profile.py.bak.1754800672
Normal file
27
services/profiles/src/app/schemas/profile.py.bak.1754800672
Normal file
@@ -0,0 +1,27 @@
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
from typing import Optional, List, Literal
|
||||
|
||||
class ProfileCreate(BaseModel):
|
||||
gender: Literal["male", "female", "other"]
|
||||
city: str
|
||||
languages: List[str] = []
|
||||
interests: List[str] = []
|
||||
|
||||
class ProfileUpdate(BaseModel):
|
||||
gender: Optional[Literal["male", "female", "other"]] = None
|
||||
city: Optional[str] = None
|
||||
languages: Optional[List[str]] = None
|
||||
interests: Optional[List[str]] = None
|
||||
photo_url: Optional[Optional[str]] = Field(default=None)
|
||||
|
||||
class ProfileOut(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
gender: Literal["male", "female", "other"]
|
||||
city: str
|
||||
languages: List[str] = []
|
||||
interests: List[str] = []
|
||||
photo_url: Optional[str] = None
|
||||
|
||||
class LikesList(BaseModel):
|
||||
items: List[str]
|
||||
21
services/profiles/src/app/services/likes_service.py
Normal file
21
services/profiles/src/app/services/likes_service.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from typing import List
|
||||
from sqlalchemy.orm import Session
|
||||
from ..repositories.likes_repository import LikesRepository
|
||||
|
||||
class LikesService:
|
||||
def __init__(self, db: Session):
|
||||
self.repo = LikesRepository(db)
|
||||
|
||||
def list_my_likes(self, user_id: str) -> List[str]:
|
||||
return self.repo.list_my_likes(user_id)
|
||||
|
||||
def put_like(self, liker: str, target: str) -> None:
|
||||
if liker == target:
|
||||
return
|
||||
self.repo.put_like(liker, target)
|
||||
|
||||
def delete_like(self, liker: str, target: str) -> None:
|
||||
self.repo.delete_like(liker, target)
|
||||
|
||||
def mutual(self, user_id: str) -> List[str]:
|
||||
return self.repo.mutual_likes(user_id)
|
||||
11
services/profiles/src/app/services/profile_search_service.py
Normal file
11
services/profiles/src/app/services/profile_search_service.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models.profile import Profile
|
||||
from ..repositories.profile_search_repository import ProfileSearchRepository
|
||||
|
||||
class ProfileSearchService:
|
||||
def __init__(self, db: Session):
|
||||
self.repo = ProfileSearchRepository(db)
|
||||
|
||||
def list_profiles(self, **kwargs) -> List[Profile]:
|
||||
return self.repo.list_profiles(**kwargs)
|
||||
@@ -1,13 +1,23 @@
|
||||
from uuid import UUID
|
||||
from app.schemas.profile import ProfileCreate
|
||||
from app.repositories.profile_repository import ProfileRepository
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models.profile import Profile
|
||||
from ..repositories.profile_repository import ProfileRepository
|
||||
|
||||
class ProfileService:
|
||||
def __init__(self, repo: ProfileRepository):
|
||||
self.repo = repo
|
||||
def __init__(self, db: Session):
|
||||
self.repo = ProfileRepository(db)
|
||||
|
||||
def get_by_user(self, user_id: UUID):
|
||||
return self.repo.get_by_user(user_id)
|
||||
def get(self, profile_id: str) -> Optional[Profile]:
|
||||
return self.repo.get(profile_id)
|
||||
|
||||
def create(self, user_id: UUID, data: ProfileCreate):
|
||||
return self.repo.create(user_id, data)
|
||||
def get_by_user_id(self, user_id: str) -> Optional[Profile]:
|
||||
return self.repo.get_by_user_id(user_id)
|
||||
|
||||
def create(self, user_id: str, **data) -> Profile:
|
||||
return self.repo.create(user_id=user_id, **data)
|
||||
|
||||
def update(self, prof: Profile, **data) -> Profile:
|
||||
return self.repo.update(prof, **data)
|
||||
|
||||
def delete(self, prof: Profile) -> None:
|
||||
return self.repo.delete(prof)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from uuid import UUID
|
||||
from app.schemas.profile import ProfileCreate
|
||||
from app.repositories.profile_repository import ProfileRepository
|
||||
|
||||
class ProfileService:
|
||||
def __init__(self, repo: ProfileRepository):
|
||||
self.repo = repo
|
||||
|
||||
def get_by_user(self, user_id: UUID):
|
||||
return self.repo.get_by_user(user_id)
|
||||
|
||||
def create(self, user_id: UUID, data: ProfileCreate):
|
||||
return self.repo.create(user_id, data)
|
||||
Reference in New Issue
Block a user