Skip to content

Commit a046163

Browse files
committed
#24 omg
1 parent 7c8ecfb commit a046163

28 files changed

+488
-197
lines changed

api/api/db/dependencies.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from typing import AsyncGenerator
22

33
from sqlalchemy.ext.asyncio import AsyncSession
4-
from starlette.requests import Request
4+
from starlette.requests import Request, HTTPConnection
55

66

7-
async def get_db_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
7+
async def get_db_session(
8+
connection: HTTPConnection,
9+
) -> AsyncGenerator[AsyncSession, None]:
810
"""
911
Create and get database session.
1012
11-
:param request: current request.
13+
:param connection: current request.
1214
:yield: database session.
1315
"""
14-
session: AsyncSession = request.app.state.db_session_factory()
16+
session: AsyncSession = connection.app.state.db_session_factory()
1517

1618
try: # noqa: WPS501
1719
yield session

api/api/db/migrations/versions/2024-02-02-07-12_1522526dbc9c.py

-27
This file was deleted.

api/api/db/migrations/versions/2024-02-02-06-45_25615d348393.py api/api/db/migrations/versions/2024-02-03-17-07_a52660341927.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
"""Add Rooms, User, RoomLinkUser Table
1+
"""add tables
22
3-
Revision ID: 25615d348393
3+
Revision ID: a52660341927
44
Revises:
5-
Create Date: 2024-02-02 06:45:34.408905
5+
Create Date: 2024-02-03 17:07:40.830802
66
77
"""
88
import sqlalchemy as sa
99
from alembic import op
1010

1111
# revision identifiers, used by Alembic.
12-
revision = "25615d348393"
12+
revision = "a52660341927"
1313
down_revision = None
1414
branch_labels = None
1515
depends_on = None

api/api/db/migrations/versions/2024-02-02-07-05_668743a5e60c.py api/api/db/migrations/versions/2024-02-03-17-52_9b57dbe84d37.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
"""Add Rooms, User, RoomLinkUser Table
1+
"""empty message
22
3-
Revision ID: 668743a5e60c
4-
Revises: 25615d348393
5-
Create Date: 2024-02-02 07:05:50.008640
3+
Revision ID: 9b57dbe84d37
4+
Revises: a52660341927
5+
Create Date: 2024-02-03 17:52:34.833449
66
77
"""
88
import sqlalchemy as sa
99
from alembic import op
1010

1111
# revision identifiers, used by Alembic.
12-
revision = "668743a5e60c"
13-
down_revision = "25615d348393"
12+
revision = "9b57dbe84d37"
13+
down_revision = "a52660341927"
1414
branch_labels = None
1515
depends_on = None
1616

api/api/db/models/associations/room_user.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ class RoomLinkUser(Base):
2121
primary_key=True,
2222
)
2323
room: Mapped["RoomModel"] = relationship(
24-
"RoomModel", back_populates="user_associations"
24+
"RoomModel",
25+
back_populates="user_associations",
26+
viewonly=True,
2527
)
2628
user: Mapped["UserModel"] = relationship(
27-
"UserModel", back_populates="room_associations"
29+
"UserModel",
30+
back_populates="room_associations",
31+
viewonly=True,
2832
)

api/api/db/models/room_model.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ class RoomModel(Base):
2323
owner_id: Mapped[UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"))
2424

2525
users: Mapped[List["UserModel"]] = relationship(
26-
secondary="room_link_user",
27-
back_populates="rooms",
26+
secondary="room_link_user", back_populates="rooms", overlaps="room"
2827
)
2928
user_associations: Mapped[List[RoomLinkUser]] = relationship(
3029
back_populates="room",
30+
overlaps="rooms",
31+
viewonly=True,
3132
)

api/api/db/models/user_model.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ class UserModel(Base):
2222
)
2323

2424
rooms: Mapped[List["RoomModel"]] = relationship(
25-
secondary="room_link_user",
26-
back_populates="users",
25+
secondary="room_link_user", back_populates="users", overlaps="user"
2726
)
2827
room_associations: Mapped[List[RoomLinkUser]] = relationship(
2928
back_populates="user",
29+
overlaps="users",
30+
viewonly=True,
3031
)

api/api/dependencies/auth.py

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
from typing import Optional
2+
from api.db.dependencies import get_db_session
23
from api.db.models.user_model import UserModel
3-
from api.schemas.user import UserInfo
44

55
from fastapi import Cookie, Depends, Header, HTTPException, status
6+
from starlette.requests import HTTPConnection
67
from jose import ExpiredSignatureError, JWTError, jwt
8+
from sqlalchemy.ext.asyncio import AsyncSession
79

810
from api.db.dao.user_dao import UserDAO
911
from api.settings import settings
1012

1113

1214
async def with_authenticate(
1315
user_dao: UserDAO = Depends(),
16+
access_token: str = Cookie(default=None),
1417
authorization: Optional[str] = Header(default=None),
1518
) -> UserModel:
16-
if authorization is None:
19+
if authorization is None and access_token is None:
1720
HTTPException(
1821
status_code=status.HTTP_401_UNAUTHORIZED,
1922
detail="Authorization credentials is missing.",
@@ -22,6 +25,8 @@ async def with_authenticate(
2225

2326
if authorization is not None:
2427
jwt_token = authorization.rsplit(maxsplit=1)[-1]
28+
elif access_token is not None:
29+
jwt_token = access_token
2530
else:
2631
raise HTTPException(
2732
status_code=401,
@@ -56,3 +61,28 @@ async def with_authenticate(
5661
detail="Not found user.",
5762
headers={"WWW-Authenticate": 'Bearer error="not_found_user"'},
5863
)
64+
65+
66+
async def ws_with_autheticate(
67+
connection: HTTPConnection,
68+
db_session: AsyncSession = Depends(get_db_session),
69+
):
70+
jwt_token = connection.cookies.get("access_token", None)
71+
72+
if jwt_token is None:
73+
return None
74+
75+
try:
76+
payload = jwt.decode(
77+
jwt_token, settings.token_secret_key, algorithms=[settings.token_algorithm]
78+
)
79+
except:
80+
return None
81+
82+
user_dao = UserDAO(db_session)
83+
user = await user_dao.get_user(payload["user_id"])
84+
85+
if user is not None:
86+
return user
87+
88+
return None

api/api/libs/errors.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class NotFoundException(Exception):
2+
pass

api/api/settings.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import enum
22
from pathlib import Path
33
from tempfile import gettempdir
4-
from typing import Literal
4+
from typing import List, Literal
55

66
from pydantic_settings import BaseSettings, SettingsConfigDict
77
from yarl import URL
@@ -36,6 +36,12 @@ class Settings(BaseSettings):
3636
port: int = 8000
3737
web_uri: str = "http://localhost:3000/"
3838

39+
origins: List[str] = [
40+
"http://localhost:3000",
41+
"http://127.0.0.1:3000",
42+
"http://web:3000",
43+
]
44+
3945
# quantity of workers for uvicorn
4046
workers_count: int = 1
4147
# Enable uvicorn reloading

api/api/web/api/token/views.py

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ async def generate_token(
1818
refresh_token: str = Cookie(default=None),
1919
user_dao: UserDAO = Depends(),
2020
) -> Response:
21-
print(refresh_token)
2221
if refresh_token is None and token_dto.refresh_token is None:
2322
raise HTTPException(
2423
status_code=status.HTTP_400_BAD_REQUEST,

api/api/web/application.py

+11
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from importlib import metadata
22
from pathlib import Path
3+
from api.settings import settings
34

45
from fastapi import FastAPI
56
from fastapi.responses import UJSONResponse
67
from fastapi.staticfiles import StaticFiles
8+
from fastapi.middleware.cors import CORSMiddleware
79

810
from api.logging import configure_logging
911
from api.web.api.router import api_router
@@ -30,6 +32,15 @@ def get_app() -> FastAPI:
3032
openapi_url="/api/openapi.json",
3133
default_response_class=UJSONResponse,
3234
)
35+
36+
app.add_middleware(
37+
CORSMiddleware,
38+
allow_origins=settings.origins,
39+
allow_credentials=True,
40+
allow_methods=["*"],
41+
allow_headers=["*"],
42+
)
43+
3344
# Adds startup and shutdown events.
3445
register_startup_event(app)
3546
register_shutdown_event(app)
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from typing import List, Set
2+
import uuid
3+
from api.db.models.room_model import RoomModel
4+
from api.db.models.user_model import UserModel
5+
from api.libs.errors import NotFoundException
6+
from api.web.ws.rooms.room import Room
7+
from api.web.ws.rooms.send.schemas import SendContent, SendJoinRoomBody
8+
from api.web.ws.rooms.user import User
9+
from fastapi import WebSocket
10+
11+
12+
class ConnectionManager:
13+
def __init__(self):
14+
self.rooms: List[Room] = []
15+
16+
async def connect(
17+
self,
18+
username: str,
19+
websocket: WebSocket,
20+
user_model: UserModel,
21+
room_model: RoomModel,
22+
):
23+
room = self.get_room_or_none(room_model.id)
24+
if room is None:
25+
room = self.create_room(room_model.id, room_model)
26+
27+
user = room.get_user_or_none(user_model.id)
28+
if user is None:
29+
user = self.create_user(
30+
username,
31+
websocket,
32+
user_model,
33+
add_room=room,
34+
)
35+
else:
36+
user.websocket = websocket
37+
38+
try:
39+
message_content = SendContent(
40+
type="join_room",
41+
body=SendJoinRoomBody(
42+
user_id=user_model.id,
43+
username=user.username,
44+
).model_dump(),
45+
)
46+
await room.send_json(message_content.model_dump(), exclude_users=[user])
47+
except:
48+
pass
49+
50+
async def disconnect(self, room_model: RoomModel, user_model: UserModel):
51+
room = self.get_room_or_none(room_model.id)
52+
if room is not None:
53+
user = room.get_user_or_none(user_model.id)
54+
if user is not None:
55+
room.leave_user(user)
56+
57+
def create_room(self, room_id: uuid.UUID, room_model: RoomModel) -> Room:
58+
room = Room(room_id, room_model)
59+
self.add_room(room)
60+
return room
61+
62+
def create_user(self, username: str, websocket: WebSocket, user_model: UserModel, add_room: Room | None = None) -> User:
63+
user = User(
64+
id=user_model.id,
65+
username=username,
66+
websocket=websocket,
67+
user_model=user_model,
68+
)
69+
70+
if add_room is not None:
71+
add_room.add_user(user)
72+
73+
return user
74+
75+
def get_room(self, room_id: uuid.UUID) -> Room:
76+
for room in self.rooms:
77+
if room.id == room_id:
78+
return room
79+
raise NotFoundException("Not found room from room_id.")
80+
81+
def get_room_or_none(
82+
self, room_uuid: uuid.UUID
83+
) -> Room | None:
84+
try:
85+
return self.get_room(room_uuid)
86+
except NotFoundException:
87+
return None
88+
89+
def add_room(self, room: Room) -> None:
90+
self.rooms.append(room)
91+
92+
def delete_room(self, room: Room) -> None:
93+
if room in self.rooms:
94+
self.rooms.discard(room)
95+
96+
def delete_room_by_id(self, room_id: uuid.UUID) -> None:
97+
room = self.get_room_by_idI(room_id)
98+
self.delete_room(room)
99+
100+
def get_room_by_id(self, uuid: uuid.UUID) -> Room | None:
101+
for room in self.rooms:
102+
if room.id == uuid:
103+
return room
104+
return None

api/api/web/ws/rooms/receive/__init__.py

Whitespace-only changes.
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import uuid
2+
from datetime import datetime
3+
from typing import Any, Literal, Union
4+
5+
from pydantic import BaseModel
6+
7+
8+
ReceiveMessageType = Literal[
9+
"chat",
10+
"join_room",
11+
"leave_room",
12+
]
13+
14+
15+
class ReceiveChatBody(BaseModel):
16+
text: str
17+
created_at: datetime
18+
19+
20+
class ReceiveJoinRoomBody(BaseModel):
21+
username: str
22+
23+
24+
class ReceiveChangeNameBody(BaseModel):
25+
username: str
26+
27+
28+
class ReceiveLeaveRoomBody(BaseModel):
29+
pass
30+
31+
32+
class ReceiveContent(BaseModel):
33+
type: ReceiveMessageType
34+
body: Any

0 commit comments

Comments
 (0)