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

Sinasezza #3

Merged
merged 2 commits into from
Jul 25, 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
Empty file removed chatApp/auth.py
Empty file.
150 changes: 150 additions & 0 deletions chatApp/config/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# auth.py
from collections.abc import Mapping
from datetime import UTC, datetime, timedelta
from typing import Any, Optional

from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from motor.motor_asyncio import AsyncIOMotorCollection
from passlib.context import CryptContext

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

settings = get_settings()

# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")

# JWT settings
SECRET_KEY = settings.jwt_secret_key.get_secret_value()
ALGORITHM = settings.jwt_algorithm
ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes


def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify if the provided password matches the stored hashed password.

:param plain_password: The plain text password.
:param hashed_password: The hashed password stored in the database.
:return: True if passwords match, otherwise False.
"""
return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password: str) -> str:
"""
Hash the given password using the password hashing context.

:param password: The plain text password to hash.
:return: The hashed password.
"""
return pwd_context.hash(password)


def create_access_token(
data: dict[str, Any], expires_delta: Optional[timedelta] = None
) -> str:
"""
Create a JWT access token with a specified expiration.

:param data: The data to encode into the token.
:param expires_delta: Optional expiration time delta for the token.
:return: The encoded JWT token as a string.
"""
to_encode = data.copy()
if expires_delta:
expire = datetime.now(UTC) + expires_delta
else:
expire = datetime.now(UTC) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})

encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt


def parse_access_token(token: str) -> dict[str, Any]:
"""
Parse and validate the given JWT token, returning its payload.

:param token: The JWT token to parse.
:return: The payload data from the token.
:raises credentials_exception: If the token is invalid or cannot be decoded.
"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError as e:
logger.error(f"JWT error: {e}") # Log the error for debugging purposes
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:
"""
Retrieve the current user from the database using the provided JWT token.

:param token: The JWT token used for authentication.
:return: The User object representing the authenticated user.
:raises credentials_exception: If the user cannot be found or the token is invalid.
"""
# Parse the token to get the payload
payload = parse_access_token(token)
username: Optional[str] = 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()

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

# Raise an exception if no user was found
if user is None:
logger.error(f"User with username {username} not found in database.")
raise credentials_exception

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


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

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

# Return None if no user was found or if password verification fails
if user is None or not verify_password(password, user["hashed_password"]):
return None

# Construct and return a User instance from the found document
return User(**user)
36 changes: 27 additions & 9 deletions chatApp/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,51 @@
from pathlib import Path

from dotenv import load_dotenv
from pydantic import Field
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict

BASE_DIR = Path(__file__).resolve().parent.parent
BASE_DIR = Path(__file__).resolve().parent.parent.parent
load_dotenv(BASE_DIR / ".env")


class Settings(BaseSettings):
debug: bool = Field(default=False)
database_url: str = "mongodb://localhost:27017"
database_name: str = "chat_app"
jwt_secret_key: str = "your-secret-key"

# database settings
database_url: str = Field(default="mongodb://localhost:27017")
database_name: str = Field(default="chat_app")
max_pool_size: int = 10
min_pool_size: int = 1

# jwt settings
jwt_secret_key: SecretStr = Field(default="your-secret-key")
jwt_algorithm: str = Field(default="HS256")
access_token_expire_minutes: int = Field(default=1440)

# CORS settings
cors_allow_origins: list[str] = Field(default=["*"])
cors_allow_credentials: bool = Field(default=True)
cors_allow_methods: list[str] = Field(default=["*"])
cors_allow_headers: list[str] = Field(default=["*"])

# logs settings
log_level: str = Field(default="INFO")
log_file_path: Path = Field(default=BASE_DIR / "logs/app.log")
log_max_bytes: int = Field(default=1048576) # 1 MB
log_backup_count: int = Field(default=3)

# upload settings
upload_dir: Path = Field(default=BASE_DIR / "uploads")
max_upload_size: int = Field(default=(5 * 1024 * 1024))

model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.log_file_path.parent.mkdir(parents=True, exist_ok=True)
self.upload_dir.mkdir(parents=True, exist_ok=True)


@lru_cache
def get_settings() -> Settings:
return Settings()


UPLOAD_DIR = BASE_DIR / "uploads"
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
60 changes: 46 additions & 14 deletions chatApp/config/database.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,55 @@
from motor.motor_asyncio import AsyncIOMotorClient
import logging
from typing import Optional

from motor.motor_asyncio import (
AsyncIOMotorClient,
AsyncIOMotorCollection,
AsyncIOMotorDatabase,
)

from .config import get_settings

logger = logging.getLogger(__name__)
settings = get_settings()

# Global variable to hold the database client
db_client = None
db = None

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

async def connect_to_mongodb(self) -> None:
try:
self.db_client = AsyncIOMotorClient(
settings.database_url,
maxPoolSize=settings.max_pool_size,
minPoolSize=settings.min_pool_size,
)

assert self.db_client is not None

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
self.db = self.db_client[settings.database_name]
assert self.db is not None

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# Initialize collections
self.users_collection = self.db.get_collection("users")
self.messages_collection = self.db.get_collection("messages")
self.rooms_collection = self.db.get_collection("rooms")

# Ping the server to validate the connection
await self.db_client.admin.command("ismaster")
logger.info("Connected to MongoDB")
except Exception as e:
logger.error(f"Could not connect to MongoDB: {e}")
raise

async def connect_to_mongodb():
global db_client, db
db_client = AsyncIOMotorClient(settings.database_url, maxPoolSize=10, minPoolSize=1)
db = db_client[settings.database_name]
print("Connected to MongoDB")
async def close_mongodb_connection(self) -> None:
if self.db_client:
self.db_client.close()
logger.info("Closed MongoDB connection")


async def close_mongodb_connection():
global db_client
if db_client:
db_client.close()
print("Closed MongoDB connection")
# Create a global instance of MongoDB
mongo_db = MongoDB()
47 changes: 47 additions & 0 deletions chatApp/config/logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import logging
from logging import Logger
from logging.handlers import RotatingFileHandler
from pathlib import Path

from .config import BASE_DIR, get_settings

settings = get_settings()

# Ensure the log directory exists
log_path = Path(BASE_DIR / settings.log_file_path)
try:
log_path.parent.mkdir(parents=True, exist_ok=True)
except Exception as e:
print(f"Error creating log directory: {e}")
raise

# Define logging format
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"

# Configure the root logger
try:
logging.basicConfig(
level=settings.log_level.upper(), # Ensure it's uppercase
format=LOG_FORMAT,
handlers=[
logging.StreamHandler(), # Log to console
RotatingFileHandler(
log_path,
maxBytes=settings.log_max_bytes,
backupCount=settings.log_backup_count,
), # Log to file with rotation
],
)
except Exception as e:
print(f"Error setting up logging: {e}")
raise

# Get a logger instance
logger: Logger = logging.getLogger(__name__)

# Example log message to test configuration
logger.info("Logging configuration is set up.")


def get_logger(name: str) -> Logger:
return logging.getLogger(name)
Empty file removed chatApp/exceptions.py
Empty file.
Empty file removed chatApp/logs.py
Empty file.
Loading
Loading