Skip to content
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
61 changes: 54 additions & 7 deletions backend/app/auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@

@router.post("/signup/email", response_model=AuthResponse)
async def signup_with_email(request: EmailSignupRequest):
"""Register a new user with email and password"""
"""
Registers a new user using email, password, and name, and returns authentication tokens and user information.

Args:
request: Contains the user's email, password, and name for registration.

Returns:
An AuthResponse with access token, refresh token, and user details.

Raises:
HTTPException: If registration fails or an unexpected error occurs.
"""
try:
result = await auth_service.create_user_with_email(
email=request.email,
Expand Down Expand Up @@ -46,7 +57,11 @@ async def signup_with_email(request: EmailSignupRequest):

@router.post("/login/email", response_model=AuthResponse)
async def login_with_email(request: EmailLoginRequest):
"""Login with email and password"""
"""
Authenticates a user using email and password credentials.

On successful authentication, returns an access token, refresh token, and user information. Raises an HTTP 500 error if authentication fails due to an unexpected error.
"""
try:
result = await auth_service.authenticate_user_with_email(
email=request.email,
Expand Down Expand Up @@ -77,7 +92,11 @@ async def login_with_email(request: EmailLoginRequest):

@router.post("/login/google", response_model=AuthResponse)
async def login_with_google(request: GoogleLoginRequest):
"""Login or signup via Google OAuth token"""
"""
Authenticates or registers a user using a Google OAuth ID token.

On success, returns an access token, refresh token, and user information. Raises an HTTP 500 error if Google authentication fails.
"""
try:
result = await auth_service.authenticate_with_google(request.id_token)

Expand Down Expand Up @@ -105,7 +124,14 @@ async def login_with_google(request: GoogleLoginRequest):

@router.post("/refresh", response_model=TokenResponse)
async def refresh_token(request: RefreshTokenRequest):
"""Refresh JWT when access token expires"""
"""
Refreshes JWT tokens using a valid refresh token.

Validates the provided refresh token, issues a new access token and refresh token if valid, and returns them. Raises a 401 error if the refresh token is invalid or revoked.

Returns:
A TokenResponse containing the new access and refresh tokens.
"""
try:
new_refresh_token = await auth_service.refresh_access_token(request.refresh_token)

Expand Down Expand Up @@ -143,7 +169,12 @@ async def refresh_token(request: RefreshTokenRequest):

@router.post("/token/verify", response_model=UserResponse)
async def verify_token(request: TokenVerifyRequest):
"""Verify access token and auto-login"""
"""
Verifies an access token and returns the associated user information.

Raises:
HTTPException: If the token is invalid or expired, returns a 401 Unauthorized error.
"""
try:
user = await auth_service.verify_access_token(request.access_token)

Expand All @@ -161,7 +192,12 @@ async def verify_token(request: TokenVerifyRequest):

@router.post("/password/reset/request", response_model=SuccessResponse)
async def request_password_reset(request: PasswordResetRequest):
"""Send password-reset email link"""
"""
Initiates a password reset process by sending a reset link to the provided email address.

Returns:
SuccessResponse: Indicates whether the password reset email was sent if the email exists.
"""
try:
await auth_service.request_password_reset(request.email)
return SuccessResponse(
Expand All @@ -176,7 +212,18 @@ async def request_password_reset(request: PasswordResetRequest):

@router.post("/password/reset/confirm", response_model=SuccessResponse)
async def confirm_password_reset(request: PasswordResetConfirm):
"""Set new password via reset token"""
"""
Resets a user's password using a valid password reset token.

Args:
request: Contains the password reset token and the new password.

Returns:
SuccessResponse indicating the password has been reset successfully.

Raises:
HTTPException: If the reset token is invalid or an error occurs during the reset process.
"""
try:
await auth_service.confirm_password_reset(
reset_token=request.reset_token,
Expand Down
55 changes: 49 additions & 6 deletions backend/app/auth/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,43 @@
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto", bcrypt__rounds=12)

def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash"""
"""
Verifies whether a plaintext password matches a given hashed password.

Args:
plain_password: The plaintext password to verify.
hashed_password: The hashed password to compare against.

Returns:
True if the plaintext password matches the hash, otherwise False.
"""
return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
"""Hash a password"""
"""
Hashes a plaintext password using bcrypt.

Args:
password: The plaintext password to hash.

Returns:
The bcrypt-hashed password as a string.
"""
return pwd_context.hash(password)

def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""Create JWT access token"""
"""
Creates a JWT access token embedding the provided data and an expiration time.

If `expires_delta` is not specified, the token expires after the default duration from settings. The payload includes an expiration timestamp and a type field set to "access". The token is signed using the configured secret key and algorithm.

Args:
data: The payload to include in the token.
expires_delta: Optional timedelta specifying how long the token is valid.

Returns:
A signed JWT access token as a string.
"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
Expand All @@ -34,11 +62,21 @@ def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta]
return encoded_jwt

def create_refresh_token() -> str:
"""Create a secure refresh token"""
"""
Generates a secure random refresh token as a URL-safe string.

Returns:
A cryptographically secure, URL-safe refresh token string.
"""
return secrets.token_urlsafe(32)

def verify_token(token: str) -> Dict[str, Any]:
"""Verify and decode JWT token"""
"""
Verifies and decodes a JWT token.

If the token is invalid or cannot be verified, raises an HTTP 401 Unauthorized exception.
Returns the decoded token payload as a dictionary.
"""
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
return payload
Expand All @@ -50,5 +88,10 @@ def verify_token(token: str) -> Dict[str, Any]:
)

def generate_reset_token() -> str:
"""Generate password reset token"""
"""
Generates a secure, URL-safe token for password reset operations.

Returns:
A random 32-byte URL-safe string suitable for use as a password reset token.
"""
return secrets.token_urlsafe(32)
101 changes: 93 additions & 8 deletions backend/app/auth/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,32 @@

class AuthService:
def __init__(self):
Initializes the AuthService instance.
pass

def get_db(self):
"""
Returns a database connection instance from the application's database module.
"""
return get_database()

async def create_user_with_email(self, email: str, password: str, name: str) -> Dict[str, Any]:
"""Create a new user with email and password"""
"""
Creates a new user account with the provided email, password, and name.

Checks for existing users with the same email and raises an error if found. Stores the user with a hashed password and default profile fields, then generates and returns a refresh token along with the user data.

Args:
email: The user's email address.
password: The user's plaintext password.
name: The user's display name.

Returns:
A dictionary containing the created user document and a refresh token.

Raises:
HTTPException: If a user with the given email already exists.
"""
db = self.get_db()

# Check if user already exists
Expand Down Expand Up @@ -70,7 +89,14 @@ async def create_user_with_email(self, email: str, password: str, name: str) ->
)

async def authenticate_user_with_email(self, email: str, password: str) -> Dict[str, Any]:
"""Authenticate user with email and password"""
"""
Authenticates a user using email and password credentials.

Verifies the provided email and password against stored user data. If authentication succeeds, returns the user information and a new refresh token. Raises an HTTP 401 error if credentials are invalid.

Returns:
A dictionary containing the authenticated user and a new refresh token.
"""
db = self.get_db()

user = await db.users.find_one({"email": email})
Expand All @@ -89,7 +115,17 @@ async def authenticate_user_with_email(self, email: str, password: str) -> Dict[
}

async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]:
"""Authenticate or create user with Google OAuth"""
"""
Authenticates a user using a Google OAuth ID token, creating a new user if necessary.

Verifies the provided Firebase ID token, retrieves or creates the corresponding user in the database, updates user information if needed, and issues a new refresh token. Raises an HTTP 400 error if the email is missing or if authentication fails, and HTTP 401 if the token is invalid.

Args:
id_token: The Firebase ID token obtained from Google OAuth.

Returns:
A dictionary containing the user data and a new refresh token.
"""
try:
# Verify the Firebase ID token
decoded_token = firebase_auth.verify_id_token(id_token)
Expand Down Expand Up @@ -163,7 +199,17 @@ async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]:
)

async def refresh_access_token(self, refresh_token: str) -> str:
"""Refresh access token using refresh token"""
"""
Refreshes an access token by validating and rotating the provided refresh token.

If the refresh token is valid and not expired, issues a new refresh token and revokes the old one. Raises an HTTP 401 error if the token is invalid, expired, or the associated user does not exist.

Args:
refresh_token: The refresh token string to validate and rotate.

Returns:
A new refresh token string.
"""
db = self.get_db()

# Find and validate refresh token
Expand Down Expand Up @@ -198,7 +244,18 @@ async def refresh_access_token(self, refresh_token: str) -> str:

return new_refresh_token
async def verify_access_token(self, token: str) -> Dict[str, Any]:
"""Verify access token and return user"""
"""
Verifies an access token and retrieves the associated user.

Args:
token: The JWT access token to verify.

Returns:
The user document corresponding to the token's subject.

Raises:
HTTPException: If the token is invalid or the user does not exist.
"""
from app.auth.security import verify_token

payload = verify_token(token)
Expand All @@ -222,7 +279,11 @@ async def verify_access_token(self, token: str) -> Dict[str, Any]:
return user

async def request_password_reset(self, email: str) -> bool:
"""Request password reset (currently just logs the reset token)"""
"""
Initiates a password reset process for the specified email address.

If the user exists, generates a password reset token with a 1-hour expiration and stores it in the database. The reset token and link are logged for development purposes. Always returns True to avoid revealing whether the email is registered.
"""
db = self.get_db()

user = await db.users.find_one({"email": email})
Expand Down Expand Up @@ -251,7 +312,21 @@ async def request_password_reset(self, email: str) -> bool:
return True

async def confirm_password_reset(self, reset_token: str, new_password: str) -> bool:
"""Confirm password reset with token"""
"""
Confirms a password reset using a valid reset token and updates the user's password.

Validates the reset token, updates the user's password, marks the token as used, and revokes all existing refresh tokens for the user to require re-authentication.

Args:
reset_token: The password reset token to validate.
new_password: The new password to set for the user.

Returns:
True if the password reset is successful.

Raises:
HTTPException: If the reset token is invalid or expired.
"""
db = self.get_db()

# Find and validate reset token
Expand Down Expand Up @@ -288,7 +363,17 @@ async def confirm_password_reset(self, reset_token: str, new_password: str) -> b

return True
async def _create_refresh_token_record(self, user_id: str) -> str:
"""Create and store refresh token"""
"""
Generates and stores a new refresh token for the specified user.

Creates a refresh token with an expiration date and saves it in the database for token management and rotation.

Args:
user_id: The unique identifier of the user for whom the refresh token is created.

Returns:
The generated refresh token string.
"""
db = self.get_db()

refresh_token = create_refresh_token()
Expand Down
19 changes: 16 additions & 3 deletions backend/app/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,30 @@ class MongoDB:
mongodb = MongoDB()

async def connect_to_mongo():
"""Create database connection"""
"""
Initializes an asynchronous connection to MongoDB and sets the active database.

Establishes a connection using the configured MongoDB URL and selects the database specified in the application settings.
"""
mongodb.client = AsyncIOMotorClient(settings.mongodb_url)
mongodb.database = mongodb.client[settings.database_name]
print("Connected to MongoDB")

async def close_mongo_connection():
"""Close database connection"""
"""
Closes the MongoDB client connection if it is currently open.

This function safely terminates the connection to the MongoDB server by closing
the existing client instance.
"""
if mongodb.client:
mongodb.client.close()
print("Disconnected from MongoDB")

def get_database():
"""Get database instance"""
"""
Returns the current MongoDB database instance.

Use this function to access the active database connection managed by the module.
"""
return mongodb.database
Loading