api development
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-08-08 21:58:36 +09:00
parent d58302c2c8
commit cc87dcc0fa
157 changed files with 14629 additions and 7 deletions

View File

@@ -0,0 +1,46 @@
from __future__ import annotations
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: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
room = ChatService(db).get_room(room_id)
if not room:
raise HTTPException(status_code=404, detail="Not found")
# NOTE: для простоты опускаем проверку участия (добавьте в проде)
return room
@router.post("/rooms/{room_id}/messages", response_model=MessageRead, status_code=201)
def send_message(room_id: str, payload: MessageCreate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ChatService(db)
room = svc.get_room(room_id)
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: 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:
raise HTTPException(status_code=404, detail="Room not found")
return svc.list_messages(room_id, offset=offset, limit=limit)

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
import os
from enum import Enum
from typing import Any, Callable
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
JWT_SECRET = os.getenv("JWT_SECRET", "devsecret_change_me")
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
class UserClaims(BaseModel):
sub: str
email: str
role: str
type: str
exp: int
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v1/token")
def decode_token(token: str) -> UserClaims:
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
return UserClaims(**payload)
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
except jwt.PyJWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
def get_current_user(token: str = Depends(oauth2_scheme)) -> UserClaims:
return decode_token(token)
def require_roles(*roles: str):
def dep(user: UserClaims = Depends(get_current_user)) -> UserClaims:
if roles and user.role not in roles:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient role")
return user
return dep

View File

@@ -1,5 +1,6 @@
from fastapi import FastAPI
from .api.routes.ping import router as ping_router
from .api.routes.chat import router as chat_router
app = FastAPI(title="CHAT Service")
@@ -7,5 +8,5 @@ app = FastAPI(title="CHAT Service")
def health():
return {"status": "ok", "service": "chat"}
# v1 API
app.include_router(ping_router, prefix="/v1")
app.include_router(chat_router)

View File

@@ -0,0 +1 @@
from .chat import ChatRoom, ChatParticipant, Message # noqa

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import String, DateTime, Text, ForeignKey, Boolean
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.db.session import Base
class ChatRoom(Base):
__tablename__ = "chat_rooms"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title: Mapped[str | None] = mapped_column(String(255), default=None)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
class ChatParticipant(Base):
__tablename__ = "chat_participants"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
room_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True, nullable=False)
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True, nullable=False)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
class Message(Base):
__tablename__ = "chat_messages"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
room_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True, nullable=False)
sender_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True, nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)

View File

@@ -0,0 +1,45 @@
from __future__ import annotations
from typing import Sequence, Optional
from sqlalchemy.orm import Session
from sqlalchemy import select, or_
from app.models.chat import ChatRoom, ChatParticipant, Message
class ChatRepository:
def __init__(self, db: Session):
self.db = db
# Rooms
def create_room(self, title: str | None) -> ChatRoom:
r = ChatRoom(title=title)
self.db.add(r)
self.db.commit()
self.db.refresh(r)
return r
def add_participant(self, room_id, user_id, is_admin: bool = False) -> ChatParticipant:
p = ChatParticipant(room_id=room_id, user_id=user_id, is_admin=is_admin)
self.db.add(p)
self.db.commit()
self.db.refresh(p)
return p
def list_rooms_for_user(self, user_id) -> Sequence[ChatRoom]:
stmt = select(ChatRoom).join(ChatParticipant, ChatParticipant.room_id == ChatRoom.id)\
.where(ChatParticipant.user_id == user_id)
return self.db.execute(stmt).scalars().all()
def get_room(self, room_id) -> Optional[ChatRoom]:
return self.db.get(ChatRoom, room_id)
# Messages
def create_message(self, room_id, sender_id, content: str) -> Message:
m = Message(room_id=room_id, sender_id=sender_id, content=content)
self.db.add(m)
self.db.commit()
self.db.refresh(m)
return m
def list_messages(self, room_id, *, offset: int = 0, limit: int = 100) -> Sequence[Message]:
stmt = select(Message).where(Message.room_id == room_id).offset(offset).limit(limit).order_by(Message.created_at.asc())
return self.db.execute(stmt).scalars().all()

View File

@@ -0,0 +1,22 @@
from __future__ import annotations
from pydantic import BaseModel, ConfigDict
from typing import Optional
class RoomCreate(BaseModel):
title: Optional[str] = None
participants: list[str] # user IDs
class RoomRead(BaseModel):
id: str
title: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class MessageCreate(BaseModel):
content: str
class MessageRead(BaseModel):
id: str
room_id: str
sender_id: str
content: str
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,31 @@
from __future__ import annotations
from sqlalchemy.orm import Session
from typing import Optional, Sequence
from app.repositories.chat_repository import ChatRepository
from app.models.chat import ChatRoom, ChatParticipant, Message
class ChatService:
def __init__(self, db: Session):
self.repo = ChatRepository(db)
def create_room(self, *, title: str | None, participant_ids: list[str], creator_id: str) -> ChatRoom:
room = self.repo.create_room(title)
# creator -> admin
self.repo.add_participant(room.id, creator_id, is_admin=True)
for uid in participant_ids:
if uid != creator_id:
self.repo.add_participant(room.id, uid, is_admin=False)
return room
def list_rooms_for_user(self, user_id: str) -> Sequence[ChatRoom]:
return self.repo.list_rooms_for_user(user_id)
def get_room(self, room_id: str) -> ChatRoom | None:
return self.repo.get_room(room_id)
def create_message(self, room_id: str, sender_id: str, content: str) -> Message:
return self.repo.create_message(room_id, sender_id, content)
def list_messages(self, room_id: str, offset: int = 0, limit: int = 100):
return self.repo.list_messages(room_id, offset=offset, limit=limit)