This commit is contained in:
46
services/chat/src/app/api/routes/chat.py
Normal file
46
services/chat/src/app/api/routes/chat.py
Normal 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)
|
||||
40
services/chat/src/app/core/security.py
Normal file
40
services/chat/src/app/core/security.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .chat import ChatRoom, ChatParticipant, Message # noqa
|
||||
|
||||
30
services/chat/src/app/models/chat.py
Normal file
30
services/chat/src/app/models/chat.py
Normal 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)
|
||||
45
services/chat/src/app/repositories/chat_repository.py
Normal file
45
services/chat/src/app/repositories/chat_repository.py
Normal 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()
|
||||
22
services/chat/src/app/schemas/chat.py
Normal file
22
services/chat/src/app/schemas/chat.py
Normal 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)
|
||||
31
services/chat/src/app/services/chat_service.py
Normal file
31
services/chat/src/app/services/chat_service.py
Normal 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)
|
||||
Reference in New Issue
Block a user