Skip to content

Commit

Permalink
Merge pull request #5 from sinasezza/sinasezza
Browse files Browse the repository at this point in the history
feat(models, routes): models changed and  some routes for send messag…
  • Loading branch information
sinasezza authored Jul 28, 2024
2 parents 2e96d4e + 9141aeb commit b2b6b2e
Show file tree
Hide file tree
Showing 14 changed files with 225 additions and 88 deletions.
40 changes: 13 additions & 27 deletions chatApp/config/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# auth.py
from collections.abc import Mapping
from datetime import UTC, datetime, timedelta
from typing import Any, Optional
from typing import Any

from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
Expand All @@ -10,9 +9,9 @@
from passlib.context import CryptContext

from chatApp.config.config import get_settings
from chatApp.config.database import mongo_db
from chatApp.config.database import get_users_collection
from chatApp.config.logs import logger
from chatApp.models.user import User
from chatApp.models.user import UserInDB
from chatApp.utils.exceptions import credentials_exception

settings = get_settings()
Expand Down Expand Up @@ -51,7 +50,7 @@ def get_password_hash(password: str) -> str:


def create_access_token(
data: dict[str, Any], expires_delta: Optional[timedelta] = None
data: dict[str, Any], expires_delta: timedelta | None = None
) -> str:
"""
Create a JWT access token with a specified expiration.
Expand Down Expand Up @@ -87,20 +86,7 @@ def parse_access_token(token: str) -> dict[str, Any]:
raise credentials_exception


def get_users_collection() -> AsyncIOMotorCollection:
"""
Retrieve the users collection from the MongoDB database.
:return: The users collection instance.
:raises RuntimeError: If the users collection is not initialized.
"""
users_collection = mongo_db.users_collection
if users_collection is None:
raise RuntimeError("Users collection is not initialized.")
return users_collection


async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
async def get_current_user(token: str = Depends(oauth2_scheme)) -> UserInDB:
"""
Retrieve the current user from the database using the provided JWT token.
Expand All @@ -110,17 +96,17 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
"""
# Parse the token to get the payload
payload = parse_access_token(token)
username: Optional[str] = payload.get("sub")
username: str | None = payload.get("sub")

if username is None:
logger.error("Username is missing in the token payload.")
raise credentials_exception

# Fetch the users_collection within the request scope
users_collection = get_users_collection()
users_collection: AsyncIOMotorCollection = get_users_collection()

# Properly type the result of the find_one query
user: Optional[Mapping[str, Any]] = await users_collection.find_one(
user: Mapping[str, Any] | None = await users_collection.find_one(
{"username": username}
)

Expand All @@ -130,15 +116,15 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
raise credentials_exception

# Construct and return a User instance from the found document
return User(**user)
return UserInDB(**user)


async def authenticate_user(username: str, password: str) -> Optional[User]:
async def authenticate_user(username: str, password: str) -> UserInDB | None:
# Fetch the users_collection within the request scope
users_collection = get_users_collection()
users_collection: AsyncIOMotorCollection = get_users_collection()

# Properly type the result of the find_one query
user: Optional[Mapping[str, Any]] = await users_collection.find_one(
user: Mapping[str, Any] | None = await users_collection.find_one(
{"username": username}
)

Expand All @@ -147,4 +133,4 @@ async def authenticate_user(username: str, password: str) -> Optional[User]:
return None

# Construct and return a User instance from the found document
return User(**user)
return UserInDB(**user)
50 changes: 44 additions & 6 deletions chatApp/config/database.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
from typing import Optional

from motor.motor_asyncio import (
AsyncIOMotorClient,
Expand All @@ -15,11 +14,11 @@

class MongoDB:
def __init__(self) -> None:
self.db_client: Optional[AsyncIOMotorClient] = None
self.db: Optional[AsyncIOMotorDatabase] = None
self.users_collection: Optional[AsyncIOMotorCollection] = None
self.messages_collection: Optional[AsyncIOMotorCollection] = None
self.rooms_collection: Optional[AsyncIOMotorCollection] = None
self.db_client: AsyncIOMotorClient | None = None
self.db: AsyncIOMotorDatabase | None = None
self.users_collection: AsyncIOMotorCollection | None = None
self.messages_collection: AsyncIOMotorCollection | None = None
self.rooms_collection: AsyncIOMotorCollection | None = None

async def connect_to_mongodb(self) -> None:
try:
Expand Down Expand Up @@ -53,3 +52,42 @@ async def close_mongodb_connection(self) -> None:

# Create a global instance of MongoDB
mongo_db = MongoDB()


def get_users_collection() -> AsyncIOMotorCollection:
"""
Retrieve the users collection from the MongoDB database.
:return: The users collection instance.
:raises RuntimeError: If the users collection is not initialized.
"""
users_collection = mongo_db.users_collection
if users_collection is None:
raise RuntimeError("Users collection is not initialized.")
return users_collection


def get_messages_collection() -> AsyncIOMotorCollection:
"""
Retrieve the messages collection from the MongoDB database.
:return: The messages collection instance.
:raises RuntimeError: If the messages collection is not initialized.
"""
messages_collection = mongo_db.messages_collection
if messages_collection is None:
raise RuntimeError("messages collection is not initialized.")
return messages_collection


def get_rooms_collection() -> AsyncIOMotorCollection:
"""
Retrieve the rooms collection from the MongoDB database.
:return: The rooms collection instance.
:raises RuntimeError: If the rooms collection is not initialized.
"""
rooms_collection = mongo_db.rooms_collection
if rooms_collection is None:
raise RuntimeError("rooms collection is not initialized.")
return rooms_collection
21 changes: 14 additions & 7 deletions chatApp/models/message.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from bson import ObjectId
from pydantic import BaseModel
from datetime import datetime, timezone

from pydantic import BaseModel, Field

from chatApp.utils.object_id import PydanticObjectId


class Message(BaseModel):
_id: ObjectId
user_id: ObjectId
room_id: str
content: str
# created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
user_id: PydanticObjectId
room_id: str | None = Field(default=None)
content: str = Field(default=None)
media: str = Field(default=None)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class MessageInDB(Message):
id: PydanticObjectId = Field(alias="_id")
14 changes: 14 additions & 0 deletions chatApp/models/room.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from datetime import datetime, timezone

from pydantic import BaseModel, Field

from chatApp.utils.object_id import PydanticObjectId


class Room(BaseModel):
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
members: list[PydanticObjectId]


class RoomInDB(Room):
id: PydanticObjectId = Field(alias="_id")
19 changes: 12 additions & 7 deletions chatApp/models/user.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
from bson import ObjectId
from pydantic import BaseModel
from datetime import datetime, timezone

from pydantic import BaseModel, Field

from chatApp.utils.object_id import PydanticObjectId


class User(BaseModel):
_id: ObjectId
username: str
email: str
hashed_password: str
is_active: bool = True
is_admin: bool = False
# created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
# updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
# last_login: datetime = None
# last_logout: datetime = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
last_login: datetime | None = None


class UserInDB(User):
id: PydanticObjectId = Field(alias="_id")
17 changes: 10 additions & 7 deletions chatApp/routes/auth.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
# auth.py
from collections.abc import Mapping
from datetime import timedelta
from typing import Any, Optional
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm

from chatApp.config import auth
from chatApp.models.user import User
from chatApp.config.database import get_users_collection
from chatApp.models.user import User, UserInDB
from chatApp.schemas.user import UserCreateSchema
from chatApp.utils.exceptions import credentials_exception

router = APIRouter()


@router.post("/register", response_model=User)
async def register_user(user: UserCreateSchema) -> User:
async def register_user(user: UserCreateSchema) -> UserInDB:
# Fetch the users_collection within the request scope
users_collection = auth.get_users_collection()
users_collection = get_users_collection()

# Check if the user already exists
existing_user: Optional[Mapping[str, Any]] = await users_collection.find_one(
existing_user: Mapping[str, Any] | None = await users_collection.find_one(
{"username": user.username}
)
if existing_user:
Expand All @@ -38,7 +39,7 @@ async def register_user(user: UserCreateSchema) -> User:
await users_collection.insert_one(user_dict)

# Construct and return a User instance from the inserted document
return User(**user_dict)
return UserInDB(**user_dict)


@router.post("/token", response_model=dict)
Expand All @@ -63,5 +64,7 @@ async def login_for_access_token(


@router.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(auth.get_current_user)) -> User:
async def read_users_me(
current_user: UserInDB = Depends(auth.get_current_user),
) -> UserInDB:
return current_user
69 changes: 68 additions & 1 deletion chatApp/routes/chat.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,70 @@
from fastapi import APIRouter
from collections.abc import Mapping
from typing import Any

from bson import ObjectId
from fastapi import APIRouter, Depends, HTTPException
from motor.motor_asyncio import AsyncIOMotorCollection, AsyncIOMotorCursor

from chatApp.config import auth
from chatApp.config.database import get_messages_collection
from chatApp.models.message import MessageInDB
from chatApp.models.user import UserInDB
from chatApp.schemas.message import MessageCreateSchema

router = APIRouter()


@router.get("/all-messages", response_model=list[MessageInDB])
async def get_all_messages():
messages_collection: AsyncIOMotorCollection = get_messages_collection()

cursor: AsyncIOMotorCursor = messages_collection.find()
messages_dicts: list[Mapping[str, Any]] = await cursor.to_list(length=None)
messages: list[MessageInDB] = [
MessageInDB(**message_dict) for message_dict in messages_dicts
]

return messages


@router.get("/messages", response_model=list[MessageInDB])
async def get_messages(user: UserInDB = Depends(auth.get_current_user)):
messages_collection: AsyncIOMotorCollection = get_messages_collection()

cursor: AsyncIOMotorCursor = messages_collection.find(
{"user_id": ObjectId(user.id)}
)
messages_dicts: list[Mapping[str, Any]] = await cursor.to_list(length=None)
messages: list[MessageInDB] = [
MessageInDB(**message_dict) for message_dict in messages_dicts
]

return messages


@router.get("/message/{message_id}", response_model=MessageInDB)
async def get_message(message_id: str, user: UserInDB = Depends(auth.get_current_user)):
messages_collection: AsyncIOMotorCollection = get_messages_collection()

message = await messages_collection.find_one(
{"_id": ObjectId(message_id), "user_id": ObjectId(user.id)}
)

if message is None:
raise HTTPException(status_code=404, detail="Message not found")

return MessageInDB(**message)


@router.post("/message", response_model=MessageInDB)
async def create_message(
message: MessageCreateSchema, user: UserInDB = Depends(auth.get_current_user)
):
messages_collection = get_messages_collection()

message_dict = message.model_dump()
message_dict["user_id"] = user.id

result = await messages_collection.insert_one(message_dict)

return MessageInDB(**message_dict, _id=result.inserted_id)
23 changes: 13 additions & 10 deletions chatApp/routes/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@
from fastapi import APIRouter
from motor.motor_asyncio import AsyncIOMotorCursor

from chatApp.config.auth import get_users_collection
from chatApp.schemas.user import users_serializer
from chatApp.config.database import get_users_collection
from chatApp.models.user import User

router = APIRouter()


@router.get("/")
async def get_users() -> list[Mapping[str, Any]]:
@router.get("/", response_model=list[User])
async def get_users() -> list[User]:
users_collection = get_users_collection()

# Perform the query to get an async cursor
cursor: AsyncIOMotorCursor = users_collection.find()
# Collect all users into a list
users: list[Mapping[str, Any]] = await cursor.to_list(
length=None
) # length=None will retrieve all documents
# Serialize the list of users
return users_serializer(users)

# Collect all users into a list of dictionaries
users_dicts: list[Mapping[str, Any]] = await cursor.to_list(length=None)

# Convert each dictionary to a User object
users: list[User] = [User(**user_dict) for user_dict in users_dicts]

return users
Loading

0 comments on commit b2b6b2e

Please sign in to comment.