Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(models, routes): models changed and some routes for send messag… #5

Merged
merged 1 commit into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading