From 23dacafcaa0ac2a519f331c5d834ee335f5ee60c Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:06:35 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`feature?= =?UTF-8?q?/auth-service-workflow`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @Devasy23. * https://github.com/Devasy23/splitwiser/pull/3#issuecomment-2977540102 The following files were modified: * `backend/app/auth/routes.py` * `backend/app/auth/security.py` * `backend/app/auth/service.py` * `backend/app/database.py` * `backend/app/dependencies.py` * `backend/generate_secret.py` * `backend/main.py` --- backend/app/auth/routes.py | 61 ++++++++++++++++++--- backend/app/auth/security.py | 55 ++++++++++++++++--- backend/app/auth/service.py | 101 ++++++++++++++++++++++++++++++++--- backend/app/database.py | 19 +++++-- backend/app/dependencies.py | 9 +++- backend/generate_secret.py | 9 +++- backend/main.py | 11 ++++ 7 files changed, 239 insertions(+), 26 deletions(-) diff --git a/backend/app/auth/routes.py b/backend/app/auth/routes.py index 964641bf..c9da4dee 100644 --- a/backend/app/auth/routes.py +++ b/backend/app/auth/routes.py @@ -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, @@ -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, @@ -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) @@ -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) @@ -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) @@ -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( @@ -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, diff --git a/backend/app/auth/security.py b/backend/app/auth/security.py index 5886e156..97849cee 100644 --- a/backend/app/auth/security.py +++ b/backend/app/auth/security.py @@ -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 @@ -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 @@ -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) diff --git a/backend/app/auth/service.py b/backend/app/auth/service.py index d14c7e99..e83705db 100644 --- a/backend/app/auth/service.py +++ b/backend/app/auth/service.py @@ -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 @@ -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}) @@ -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) @@ -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 @@ -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) @@ -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}) @@ -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 @@ -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() diff --git a/backend/app/database.py b/backend/app/database.py index 14cfa1b2..bf50ab72 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -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 diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 0bff65e1..39679e7c 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -7,7 +7,14 @@ security = HTTPBearer() async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]: - """Get current authenticated user from JWT token""" + """ + Retrieves the currently authenticated user based on a JWT token from the HTTP Authorization header. + + Verifies the provided JWT token, extracts the user ID, and fetches the corresponding user document from the database. Raises an HTTP 401 Unauthorized error if the token is invalid, the user ID is missing, or the user does not exist. + + Returns: + A dictionary representing the authenticated user, with the `_id` field as a string. + """ try: # Verify token payload = verify_token(credentials.credentials) diff --git a/backend/generate_secret.py b/backend/generate_secret.py index 89e64442..284e5621 100644 --- a/backend/generate_secret.py +++ b/backend/generate_secret.py @@ -2,7 +2,14 @@ import string def generate_jwt_secret(): - """Generate a secure JWT secret key""" + """ + Generates a cryptographically secure 64-character secret key for JWT authentication. + + The key consists of uppercase and lowercase letters, digits, and the special characters "!@#$%^&*". + + Returns: + A randomly generated 64-character string suitable for use as a JWT secret key. + """ # Generate a 64-character secret key alphabet = string.ascii_letters + string.digits + "!@#$%^&*" secret_key = ''.join(secrets.choice(alphabet) for _ in range(64)) diff --git a/backend/main.py b/backend/main.py index b6caab0e..caee5da1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -24,15 +24,26 @@ # Database events @app.on_event("startup") async def startup_event(): + """ + Initializes the MongoDB connection when the application starts. + """ await connect_to_mongo() @app.on_event("shutdown") async def shutdown_event(): + """ + Closes the MongoDB connection when the application shuts down. + """ await close_mongo_connection() # Health check @app.get("/health") async def health_check(): + """ + Returns the health status of the Splitwiser API service. + + This endpoint can be used for health checks and monitoring. + """ return {"status": "healthy", "service": "Splitwiser API"} # Include routers