|
|
|
|
@@ -5,442 +5,510 @@ This module provides the main authentication service that orchestrates
|
|
|
|
|
all authentication operations including registration, login, token management,
|
|
|
|
|
password reset, and email verification.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
from .password import PasswordManager
|
|
|
|
|
from .token import TokenManager
|
|
|
|
|
from ..persistence.base import UserRepository, TokenRepository
|
|
|
|
|
from ..models.user import UserCreate, UserInDB, UserUpdate
|
|
|
|
|
from ..models.token import AccessTokenResponse, TokenData
|
|
|
|
|
from ..emailing.base import EmailService
|
|
|
|
|
from ..exceptions import (
|
|
|
|
|
InvalidCredentialsError,
|
|
|
|
|
UserNotFoundError,
|
|
|
|
|
AccountDisabledError,
|
|
|
|
|
ExpiredTokenError,
|
|
|
|
|
InvalidTokenError,
|
|
|
|
|
RevokedTokenError
|
|
|
|
|
InvalidCredentialsError,
|
|
|
|
|
UserNotFoundError,
|
|
|
|
|
AccountDisabledError,
|
|
|
|
|
ExpiredTokenError,
|
|
|
|
|
InvalidTokenError,
|
|
|
|
|
RevokedTokenError
|
|
|
|
|
)
|
|
|
|
|
from ..models.token import AccessTokenResponse, TokenData
|
|
|
|
|
from ..models.user import UserCreate, UserInDB, UserUpdate, UserCreateNoValidation
|
|
|
|
|
from ..persistence.base import UserRepository, TokenRepository
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AuthService:
|
|
|
|
|
"""
|
|
|
|
|
Main authentication service.
|
|
|
|
|
|
|
|
|
|
This service orchestrates all authentication-related operations by
|
|
|
|
|
coordinating between password management, token management, and
|
|
|
|
|
persistence layers.
|
|
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
|
user_repository: Repository for user persistence operations.
|
|
|
|
|
token_repository: Repository for token persistence operations.
|
|
|
|
|
password_manager: Manager for password hashing and verification.
|
|
|
|
|
token_manager: Manager for token creation and validation.
|
|
|
|
|
email_service: Optional service for sending emails.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
user_repository: UserRepository,
|
|
|
|
|
token_repository: TokenRepository,
|
|
|
|
|
password_manager: PasswordManager,
|
|
|
|
|
token_manager: TokenManager,
|
|
|
|
|
email_service: Optional[EmailService] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Main authentication service.
|
|
|
|
|
Initialize the authentication service.
|
|
|
|
|
|
|
|
|
|
This service orchestrates all authentication-related operations by
|
|
|
|
|
coordinating between password management, token management, and
|
|
|
|
|
persistence layers.
|
|
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
|
user_repository: Repository for user persistence operations.
|
|
|
|
|
token_repository: Repository for token persistence operations.
|
|
|
|
|
Args:
|
|
|
|
|
user_repository: Repository for user persistence.
|
|
|
|
|
token_repository: Repository for token persistence.
|
|
|
|
|
password_manager: Manager for password hashing and verification.
|
|
|
|
|
token_manager: Manager for token creation and validation.
|
|
|
|
|
email_service: Optional service for sending emails.
|
|
|
|
|
email_service: Optional service for sending emails (password reset, verification).
|
|
|
|
|
"""
|
|
|
|
|
self.user_repository = user_repository
|
|
|
|
|
self.token_repository = token_repository
|
|
|
|
|
self.password_manager = password_manager
|
|
|
|
|
self.token_manager = token_manager
|
|
|
|
|
self.email_service = email_service
|
|
|
|
|
|
|
|
|
|
def create_admin_if_needed(self, admin_email: str = None, admin_username: str = None, admin_password: str = None):
|
|
|
|
|
"""
|
|
|
|
|
Create the admin user if it does not exist. This function checks the current number
|
|
|
|
|
of users in the system. If there are no users present, it creates a user with
|
|
|
|
|
administrative privileges using the provided credentials or environment variables
|
|
|
|
|
as defaults.
|
|
|
|
|
|
|
|
|
|
:param admin_email: The email of the admin user. Defaults to "AUTH_ADMIN_EMAIL"
|
|
|
|
|
environment variable or "admin@myauth.com" if not provided.
|
|
|
|
|
:type admin_email: str, optional
|
|
|
|
|
:param admin_username: The username of the admin user. Defaults to
|
|
|
|
|
"AUTH_ADMIN_USERNAME" environment variable or "admin" if not provided.
|
|
|
|
|
:type admin_username: str, optional
|
|
|
|
|
:param admin_password: The password of the admin user. Defaults to
|
|
|
|
|
"AUTH_ADMIN_PASSWORD" environment variable or "admin" if not provided.
|
|
|
|
|
:type admin_password: str, optional
|
|
|
|
|
:return: True if an admin user is created, otherwise False.
|
|
|
|
|
:rtype: bool
|
|
|
|
|
"""
|
|
|
|
|
# create the admin user if it doesn't exist
|
|
|
|
|
nb_users = self.count_users()
|
|
|
|
|
if nb_users == 0:
|
|
|
|
|
admin_email = admin_email or os.getenv("AUTH_ADMIN_EMAIL", "admin@myauth.com")
|
|
|
|
|
admin_username = admin_username or os.getenv("AUTH_ADMIN_USERNAME", "admin")
|
|
|
|
|
admin_password = admin_password or os.getenv("AUTH_ADMIN_PASSWORD", "admin")
|
|
|
|
|
|
|
|
|
|
admin_user = UserCreateNoValidation(
|
|
|
|
|
email=admin_email,
|
|
|
|
|
username=admin_username,
|
|
|
|
|
password=admin_password,
|
|
|
|
|
roles=["admin"]
|
|
|
|
|
)
|
|
|
|
|
self.register(admin_user)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
user_repository: UserRepository,
|
|
|
|
|
token_repository: TokenRepository,
|
|
|
|
|
password_manager: PasswordManager,
|
|
|
|
|
token_manager: TokenManager,
|
|
|
|
|
email_service: Optional[EmailService] = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Initialize the authentication service.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user_repository: Repository for user persistence.
|
|
|
|
|
token_repository: Repository for token persistence.
|
|
|
|
|
password_manager: Manager for password hashing and verification.
|
|
|
|
|
token_manager: Manager for token creation and validation.
|
|
|
|
|
email_service: Optional service for sending emails (password reset, verification).
|
|
|
|
|
"""
|
|
|
|
|
self.user_repository = user_repository
|
|
|
|
|
self.token_repository = token_repository
|
|
|
|
|
self.password_manager = password_manager
|
|
|
|
|
self.token_manager = token_manager
|
|
|
|
|
self.email_service = email_service
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def register(self, user_data: UserCreate | UserCreateNoValidation) -> UserInDB:
|
|
|
|
|
"""
|
|
|
|
|
Register a new user.
|
|
|
|
|
|
|
|
|
|
def register(self, user_data: UserCreate) -> UserInDB:
|
|
|
|
|
"""
|
|
|
|
|
Register a new user.
|
|
|
|
|
|
|
|
|
|
This method creates a new user account with hashed password.
|
|
|
|
|
The user's email is initially unverified.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user_data: User registration data including password.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The created user (without password).
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
UserAlreadyExistsError: If email is already registered.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> user_data = UserCreate(
|
|
|
|
|
... email="user@example.com",
|
|
|
|
|
... username="john_doe",
|
|
|
|
|
... password="SecurePass123!"
|
|
|
|
|
... )
|
|
|
|
|
>>> user = auth_service.register(user_data)
|
|
|
|
|
"""
|
|
|
|
|
hashed_password = self.password_manager.hash_password(user_data.password)
|
|
|
|
|
user = self.user_repository.create_user(user_data, hashed_password)
|
|
|
|
|
return user
|
|
|
|
|
This method creates a new user account with hashed password.
|
|
|
|
|
The user's email is initially unverified.
|
|
|
|
|
|
|
|
|
|
def login(self, email: str, password: str) -> tuple[UserInDB, AccessTokenResponse]:
|
|
|
|
|
"""
|
|
|
|
|
Authenticate a user and create tokens.
|
|
|
|
|
Args:
|
|
|
|
|
user_data: User registration data including password.
|
|
|
|
|
|
|
|
|
|
This method verifies credentials, checks account status, and generates
|
|
|
|
|
both access and refresh tokens.
|
|
|
|
|
Returns:
|
|
|
|
|
The created user (without password).
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
email: User's email address.
|
|
|
|
|
password: User's plain text password.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Tuple of (user, tokens) where tokens contains access_token and refresh_token.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidCredentialsError: If email or password is incorrect.
|
|
|
|
|
AccountDisabledError: If user account is disabled.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> user, tokens = auth_service.login("user@example.com", "password")
|
|
|
|
|
>>> print(tokens.access_token)
|
|
|
|
|
"""
|
|
|
|
|
# Get user by email
|
|
|
|
|
user = self.user_repository.get_user_by_email(email)
|
|
|
|
|
if not user:
|
|
|
|
|
raise InvalidCredentialsError()
|
|
|
|
|
Raises:
|
|
|
|
|
UserAlreadyExistsError: If email is already registered.
|
|
|
|
|
|
|
|
|
|
# Verify password
|
|
|
|
|
if not self.password_manager.verify_password(password, user.hashed_password):
|
|
|
|
|
raise InvalidCredentialsError()
|
|
|
|
|
|
|
|
|
|
# Check if account is active
|
|
|
|
|
if not user.is_active:
|
|
|
|
|
raise AccountDisabledError()
|
|
|
|
|
|
|
|
|
|
# Create tokens
|
|
|
|
|
access_token = self.token_manager.create_access_token(user)
|
|
|
|
|
refresh_token = self.token_manager.create_refresh_token()
|
|
|
|
|
|
|
|
|
|
# Store refresh token in database
|
|
|
|
|
token_data = TokenData(
|
|
|
|
|
token=refresh_token,
|
|
|
|
|
token_type="refresh",
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
expires_at=self.token_manager.get_refresh_token_expiration(),
|
|
|
|
|
created_at=datetime.utcnow(),
|
|
|
|
|
is_revoked=False
|
|
|
|
|
)
|
|
|
|
|
self.token_repository.save_token(token_data)
|
|
|
|
|
|
|
|
|
|
tokens = AccessTokenResponse(
|
|
|
|
|
access_token=access_token,
|
|
|
|
|
refresh_token=refresh_token,
|
|
|
|
|
token_type="bearer"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return user, tokens
|
|
|
|
|
Example:
|
|
|
|
|
>>> user_data = UserCreate(
|
|
|
|
|
... email="user@example.com",
|
|
|
|
|
... username="john_doe",
|
|
|
|
|
... password="SecurePass123!"
|
|
|
|
|
... )
|
|
|
|
|
>>> user = auth_service.register(user_data)
|
|
|
|
|
"""
|
|
|
|
|
hashed_password = self.password_manager.hash_password(user_data.password)
|
|
|
|
|
user = self.user_repository.create_user(user_data, hashed_password)
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
def login(self, email: str, password: str) -> tuple[UserInDB, AccessTokenResponse]:
|
|
|
|
|
"""
|
|
|
|
|
Authenticate a user and create tokens.
|
|
|
|
|
|
|
|
|
|
def refresh_access_token(self, refresh_token: str) -> AccessTokenResponse:
|
|
|
|
|
"""
|
|
|
|
|
Create a new access token using a refresh token.
|
|
|
|
|
|
|
|
|
|
This method validates the refresh token and generates a new access token
|
|
|
|
|
without requiring the user to re-enter their password.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
refresh_token: The refresh token to exchange.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
New access and refresh tokens.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidTokenError: If refresh token is invalid.
|
|
|
|
|
ExpiredTokenError: If refresh token has expired.
|
|
|
|
|
RevokedTokenError: If refresh token has been revoked.
|
|
|
|
|
UserNotFoundError: If user no longer exists.
|
|
|
|
|
AccountDisabledError: If user account is disabled.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> tokens = auth_service.refresh_access_token(old_refresh_token)
|
|
|
|
|
>>> print(tokens.access_token)
|
|
|
|
|
"""
|
|
|
|
|
# Validate refresh token
|
|
|
|
|
token_data = self.token_repository.get_token(refresh_token, "refresh")
|
|
|
|
|
if not token_data:
|
|
|
|
|
raise InvalidTokenError("Invalid refresh token")
|
|
|
|
|
|
|
|
|
|
if token_data.is_revoked:
|
|
|
|
|
raise RevokedTokenError()
|
|
|
|
|
|
|
|
|
|
if token_data.expires_at < datetime.utcnow():
|
|
|
|
|
raise ExpiredTokenError()
|
|
|
|
|
|
|
|
|
|
# Get user
|
|
|
|
|
user = self.user_repository.get_user_by_id(token_data.user_id)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError()
|
|
|
|
|
|
|
|
|
|
if not user.is_active:
|
|
|
|
|
raise AccountDisabledError()
|
|
|
|
|
|
|
|
|
|
# Create new tokens
|
|
|
|
|
access_token = self.token_manager.create_access_token(user)
|
|
|
|
|
new_refresh_token = self.token_manager.create_refresh_token()
|
|
|
|
|
|
|
|
|
|
# Revoke old refresh token
|
|
|
|
|
self.token_repository.revoke_token(refresh_token)
|
|
|
|
|
|
|
|
|
|
# Store new refresh token
|
|
|
|
|
new_token_data = TokenData(
|
|
|
|
|
token=new_refresh_token,
|
|
|
|
|
token_type="refresh",
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
expires_at=self.token_manager.get_refresh_token_expiration(),
|
|
|
|
|
created_at=datetime.utcnow(),
|
|
|
|
|
is_revoked=False
|
|
|
|
|
)
|
|
|
|
|
self.token_repository.save_token(new_token_data)
|
|
|
|
|
|
|
|
|
|
return AccessTokenResponse(
|
|
|
|
|
access_token=access_token,
|
|
|
|
|
refresh_token=new_refresh_token,
|
|
|
|
|
token_type="bearer"
|
|
|
|
|
)
|
|
|
|
|
This method verifies credentials, checks account status, and generates
|
|
|
|
|
both access and refresh tokens.
|
|
|
|
|
|
|
|
|
|
def logout(self, refresh_token: str) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Logout a user by revoking their refresh token.
|
|
|
|
|
Args:
|
|
|
|
|
email: User's email address.
|
|
|
|
|
password: User's plain text password.
|
|
|
|
|
|
|
|
|
|
This prevents the refresh token from being used to obtain new
|
|
|
|
|
access tokens. The current access token will remain valid until
|
|
|
|
|
it expires naturally.
|
|
|
|
|
Returns:
|
|
|
|
|
Tuple of (user, tokens) where tokens contains access_token and refresh_token.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
refresh_token: The refresh token to revoke.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if logout was successful, False if token not found.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> auth_service.logout(refresh_token)
|
|
|
|
|
True
|
|
|
|
|
"""
|
|
|
|
|
return self.token_repository.revoke_token(refresh_token)
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidCredentialsError: If email or password is incorrect.
|
|
|
|
|
AccountDisabledError: If user account is disabled.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> user, tokens = auth_service.login("user@example.com", "password")
|
|
|
|
|
>>> print(tokens.access_token)
|
|
|
|
|
"""
|
|
|
|
|
# Get user by email
|
|
|
|
|
user = self.user_repository.get_user_by_email(email)
|
|
|
|
|
if not user:
|
|
|
|
|
raise InvalidCredentialsError()
|
|
|
|
|
|
|
|
|
|
def request_password_reset(self, email: str) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Generate a password reset token for a user.
|
|
|
|
|
|
|
|
|
|
This method creates a secure token that can be sent to the user
|
|
|
|
|
via email to reset their password. If an email service is configured,
|
|
|
|
|
the email is sent automatically. Otherwise, the token is returned
|
|
|
|
|
for manual handling.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
email: User's email address.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The password reset token to be sent via email.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
UserNotFoundError: If email is not registered.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> token = auth_service.request_password_reset("user@example.com")
|
|
|
|
|
>>> # Token is automatically sent via email if service is configured
|
|
|
|
|
"""
|
|
|
|
|
user = self.user_repository.get_user_by_email(email)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError(f"No user found with email {email}")
|
|
|
|
|
|
|
|
|
|
# Create reset token
|
|
|
|
|
reset_token = self.token_manager.create_password_reset_token()
|
|
|
|
|
|
|
|
|
|
# Store token in database
|
|
|
|
|
token_data = TokenData(
|
|
|
|
|
token=reset_token,
|
|
|
|
|
token_type="password_reset",
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
expires_at=self.token_manager.get_password_reset_token_expiration(),
|
|
|
|
|
created_at=datetime.utcnow(),
|
|
|
|
|
is_revoked=False
|
|
|
|
|
)
|
|
|
|
|
self.token_repository.save_token(token_data)
|
|
|
|
|
|
|
|
|
|
# Send email if service is configured
|
|
|
|
|
if self.email_service:
|
|
|
|
|
self.email_service.send_password_reset_email(email, reset_token)
|
|
|
|
|
|
|
|
|
|
return reset_token
|
|
|
|
|
# Verify password
|
|
|
|
|
if not self.password_manager.verify_password(password, user.hashed_password):
|
|
|
|
|
raise InvalidCredentialsError()
|
|
|
|
|
|
|
|
|
|
def reset_password(self, token: str, new_password: str) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Reset a user's password using a reset token.
|
|
|
|
|
|
|
|
|
|
This method validates the reset token and updates the user's password.
|
|
|
|
|
All existing refresh tokens for the user are revoked for security.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
token: Password reset token.
|
|
|
|
|
new_password: New plain text password (will be hashed).
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if password was reset successfully.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidTokenError: If reset token is invalid.
|
|
|
|
|
ExpiredTokenError: If reset token has expired.
|
|
|
|
|
RevokedTokenError: If reset token has been used.
|
|
|
|
|
UserNotFoundError: If user no longer exists.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> auth_service.reset_password(token, "NewSecurePass123!")
|
|
|
|
|
True
|
|
|
|
|
"""
|
|
|
|
|
# Validate reset token
|
|
|
|
|
token_data = self.token_repository.get_token(token, "password_reset")
|
|
|
|
|
if not token_data:
|
|
|
|
|
raise InvalidTokenError("Invalid password reset token")
|
|
|
|
|
|
|
|
|
|
if token_data.is_revoked:
|
|
|
|
|
raise RevokedTokenError("Password reset token has already been used")
|
|
|
|
|
|
|
|
|
|
if token_data.expires_at < datetime.utcnow():
|
|
|
|
|
raise ExpiredTokenError("Password reset token has expired")
|
|
|
|
|
|
|
|
|
|
# Get user
|
|
|
|
|
user = self.user_repository.get_user_by_id(token_data.user_id)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError()
|
|
|
|
|
|
|
|
|
|
# Hash new password
|
|
|
|
|
hashed_password = self.password_manager.hash_password(new_password)
|
|
|
|
|
|
|
|
|
|
# Update user password
|
|
|
|
|
updates = UserUpdate(password=hashed_password)
|
|
|
|
|
self.user_repository.update_user(user.id, updates)
|
|
|
|
|
|
|
|
|
|
# Revoke the reset token
|
|
|
|
|
self.token_repository.revoke_token(token)
|
|
|
|
|
|
|
|
|
|
# Revoke all user's refresh tokens for security
|
|
|
|
|
self.token_repository.revoke_all_user_tokens(user.id, "refresh")
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
# Check if account is active
|
|
|
|
|
if not user.is_active:
|
|
|
|
|
raise AccountDisabledError()
|
|
|
|
|
|
|
|
|
|
def request_email_verification(self, email: str) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Generate an email verification token for a user.
|
|
|
|
|
|
|
|
|
|
This method creates a JWT token that can be sent to the user
|
|
|
|
|
to verify their email address. If an email service is configured,
|
|
|
|
|
the email is sent automatically. Otherwise, the token is returned
|
|
|
|
|
for manual handling.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
email: User's email address.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The email verification token (JWT) to be sent via email.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
UserNotFoundError: If email is not registered.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> token = auth_service.request_email_verification("user@example.com")
|
|
|
|
|
>>> # Token is automatically sent via email if service is configured
|
|
|
|
|
"""
|
|
|
|
|
user = self.user_repository.get_user_by_email(email)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError(f"No user found with email {email}")
|
|
|
|
|
|
|
|
|
|
verification_token = self.token_manager.create_email_verification_token(email)
|
|
|
|
|
|
|
|
|
|
# Send email if service is configured
|
|
|
|
|
if self.email_service:
|
|
|
|
|
self.email_service.send_verification_email(email, verification_token)
|
|
|
|
|
|
|
|
|
|
return verification_token
|
|
|
|
|
# Create tokens
|
|
|
|
|
access_token = self.token_manager.create_access_token(user)
|
|
|
|
|
refresh_token = self.token_manager.create_refresh_token()
|
|
|
|
|
|
|
|
|
|
def verify_email(self, token: str) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Verify a user's email address using a verification token.
|
|
|
|
|
|
|
|
|
|
This method decodes the JWT token, extracts the email, and marks
|
|
|
|
|
the user's email as verified.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
token: Email verification token (JWT).
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if email was verified successfully.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidTokenError: If verification token is invalid.
|
|
|
|
|
ExpiredTokenError: If verification token has expired.
|
|
|
|
|
UserNotFoundError: If user no longer exists.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> auth_service.verify_email(token)
|
|
|
|
|
True
|
|
|
|
|
"""
|
|
|
|
|
# Decode and validate token
|
|
|
|
|
email = self.token_manager.decode_email_verification_token(token)
|
|
|
|
|
|
|
|
|
|
# Get user
|
|
|
|
|
user = self.user_repository.get_user_by_email(email)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError(f"No user found with email {email}")
|
|
|
|
|
|
|
|
|
|
# Update user's verified status
|
|
|
|
|
updates = UserUpdate(is_verified=True)
|
|
|
|
|
self.user_repository.update_user(user.id, updates)
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
# Store refresh token in database
|
|
|
|
|
token_data = TokenData(
|
|
|
|
|
token=refresh_token,
|
|
|
|
|
token_type="refresh",
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
expires_at=self.token_manager.get_refresh_token_expiration(),
|
|
|
|
|
created_at=datetime.utcnow(),
|
|
|
|
|
is_revoked=False
|
|
|
|
|
)
|
|
|
|
|
self.token_repository.save_token(token_data)
|
|
|
|
|
|
|
|
|
|
def get_current_user(self, access_token: str) -> UserInDB:
|
|
|
|
|
"""
|
|
|
|
|
Retrieve the current user from an access token.
|
|
|
|
|
tokens = AccessTokenResponse(
|
|
|
|
|
access_token=access_token,
|
|
|
|
|
refresh_token=refresh_token,
|
|
|
|
|
token_type="bearer"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return user, tokens
|
|
|
|
|
|
|
|
|
|
def refresh_access_token(self, refresh_token: str) -> AccessTokenResponse:
|
|
|
|
|
"""
|
|
|
|
|
Create a new access token using a refresh token.
|
|
|
|
|
|
|
|
|
|
This method validates the refresh token and generates a new access token
|
|
|
|
|
without requiring the user to re-enter their password.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
refresh_token: The refresh token to exchange.
|
|
|
|
|
|
|
|
|
|
This method decodes and validates the JWT access token and
|
|
|
|
|
retrieves the corresponding user from the database.
|
|
|
|
|
Returns:
|
|
|
|
|
New access and refresh tokens.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
access_token: JWT access token.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The user associated with the token.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidTokenError: If access token is invalid.
|
|
|
|
|
ExpiredTokenError: If access token has expired.
|
|
|
|
|
UserNotFoundError: If user no longer exists.
|
|
|
|
|
AccountDisabledError: If user account is disabled.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> user = auth_service.get_current_user(access_token)
|
|
|
|
|
>>> print(user.email)
|
|
|
|
|
"""
|
|
|
|
|
# Decode and validate token
|
|
|
|
|
token_payload = self.token_manager.decode_access_token(access_token)
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidTokenError: If refresh token is invalid.
|
|
|
|
|
ExpiredTokenError: If refresh token has expired.
|
|
|
|
|
RevokedTokenError: If refresh token has been revoked.
|
|
|
|
|
UserNotFoundError: If user no longer exists.
|
|
|
|
|
AccountDisabledError: If user account is disabled.
|
|
|
|
|
|
|
|
|
|
# Get user
|
|
|
|
|
user = self.user_repository.get_user_by_id(token_payload.sub)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError()
|
|
|
|
|
Example:
|
|
|
|
|
>>> tokens = auth_service.refresh_access_token(old_refresh_token)
|
|
|
|
|
>>> print(tokens.access_token)
|
|
|
|
|
"""
|
|
|
|
|
# Validate refresh token
|
|
|
|
|
token_data = self.token_repository.get_token(refresh_token, "refresh")
|
|
|
|
|
if not token_data:
|
|
|
|
|
raise InvalidTokenError("Invalid refresh token")
|
|
|
|
|
|
|
|
|
|
if token_data.is_revoked:
|
|
|
|
|
raise RevokedTokenError()
|
|
|
|
|
|
|
|
|
|
if token_data.expires_at < datetime.utcnow():
|
|
|
|
|
raise ExpiredTokenError()
|
|
|
|
|
|
|
|
|
|
# Get user
|
|
|
|
|
user = self.user_repository.get_user_by_id(token_data.user_id)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError()
|
|
|
|
|
|
|
|
|
|
if not user.is_active:
|
|
|
|
|
raise AccountDisabledError()
|
|
|
|
|
|
|
|
|
|
# Create new tokens
|
|
|
|
|
access_token = self.token_manager.create_access_token(user)
|
|
|
|
|
new_refresh_token = self.token_manager.create_refresh_token()
|
|
|
|
|
|
|
|
|
|
# Revoke old refresh token
|
|
|
|
|
self.token_repository.revoke_token(refresh_token)
|
|
|
|
|
|
|
|
|
|
# Store new refresh token
|
|
|
|
|
new_token_data = TokenData(
|
|
|
|
|
token=new_refresh_token,
|
|
|
|
|
token_type="refresh",
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
expires_at=self.token_manager.get_refresh_token_expiration(),
|
|
|
|
|
created_at=datetime.utcnow(),
|
|
|
|
|
is_revoked=False
|
|
|
|
|
)
|
|
|
|
|
self.token_repository.save_token(new_token_data)
|
|
|
|
|
|
|
|
|
|
return AccessTokenResponse(
|
|
|
|
|
access_token=access_token,
|
|
|
|
|
refresh_token=new_refresh_token,
|
|
|
|
|
token_type="bearer"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def logout(self, refresh_token: str) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Logout a user by revoking their refresh token.
|
|
|
|
|
|
|
|
|
|
This prevents the refresh token from being used to obtain new
|
|
|
|
|
access tokens. The current access token will remain valid until
|
|
|
|
|
it expires naturally.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
refresh_token: The refresh token to revoke.
|
|
|
|
|
|
|
|
|
|
if not user.is_active:
|
|
|
|
|
raise AccountDisabledError()
|
|
|
|
|
Returns:
|
|
|
|
|
True if logout was successful, False if token not found.
|
|
|
|
|
|
|
|
|
|
return user
|
|
|
|
|
Example:
|
|
|
|
|
>>> auth_service.logout(refresh_token)
|
|
|
|
|
True
|
|
|
|
|
"""
|
|
|
|
|
return self.token_repository.revoke_token(refresh_token)
|
|
|
|
|
|
|
|
|
|
def request_password_reset(self, email: str) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Generate a password reset token for a user.
|
|
|
|
|
|
|
|
|
|
This method creates a secure token that can be sent to the user
|
|
|
|
|
via email to reset their password. If an email service is configured,
|
|
|
|
|
the email is sent automatically. Otherwise, the token is returned
|
|
|
|
|
for manual handling.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
email: User's email address.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The password reset token to be sent via email.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
UserNotFoundError: If email is not registered.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> token = auth_service.request_password_reset("user@example.com")
|
|
|
|
|
>>> # Token is automatically sent via email if service is configured
|
|
|
|
|
"""
|
|
|
|
|
user = self.user_repository.get_user_by_email(email)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError(f"No user found with email {email}")
|
|
|
|
|
|
|
|
|
|
# Create reset token
|
|
|
|
|
reset_token = self.token_manager.create_password_reset_token()
|
|
|
|
|
|
|
|
|
|
# Store token in database
|
|
|
|
|
token_data = TokenData(
|
|
|
|
|
token=reset_token,
|
|
|
|
|
token_type="password_reset",
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
expires_at=self.token_manager.get_password_reset_token_expiration(),
|
|
|
|
|
created_at=datetime.utcnow(),
|
|
|
|
|
is_revoked=False
|
|
|
|
|
)
|
|
|
|
|
self.token_repository.save_token(token_data)
|
|
|
|
|
|
|
|
|
|
# Send email if service is configured
|
|
|
|
|
if self.email_service:
|
|
|
|
|
self.email_service.send_password_reset_email(email, reset_token)
|
|
|
|
|
|
|
|
|
|
return reset_token
|
|
|
|
|
|
|
|
|
|
def reset_password(self, token: str, new_password: str) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Reset a user's password using a reset token.
|
|
|
|
|
|
|
|
|
|
This method validates the reset token and updates the user's password.
|
|
|
|
|
All existing refresh tokens for the user are revoked for security.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
token: Password reset token.
|
|
|
|
|
new_password: New plain text password (will be hashed).
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if password was reset successfully.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidTokenError: If reset token is invalid.
|
|
|
|
|
ExpiredTokenError: If reset token has expired.
|
|
|
|
|
RevokedTokenError: If reset token has been used.
|
|
|
|
|
UserNotFoundError: If user no longer exists.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> auth_service.reset_password(token, "NewSecurePass123!")
|
|
|
|
|
True
|
|
|
|
|
"""
|
|
|
|
|
# Validate reset token
|
|
|
|
|
token_data = self.token_repository.get_token(token, "password_reset")
|
|
|
|
|
if not token_data:
|
|
|
|
|
raise InvalidTokenError("Invalid password reset token")
|
|
|
|
|
|
|
|
|
|
if token_data.is_revoked:
|
|
|
|
|
raise RevokedTokenError("Password reset token has already been used")
|
|
|
|
|
|
|
|
|
|
if token_data.expires_at < datetime.utcnow():
|
|
|
|
|
raise ExpiredTokenError("Password reset token has expired")
|
|
|
|
|
|
|
|
|
|
# Get user
|
|
|
|
|
user = self.user_repository.get_user_by_id(token_data.user_id)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError()
|
|
|
|
|
|
|
|
|
|
# Hash new password
|
|
|
|
|
hashed_password = self.password_manager.hash_password(new_password)
|
|
|
|
|
|
|
|
|
|
# Update user password
|
|
|
|
|
updates = UserUpdate(password=hashed_password)
|
|
|
|
|
self.user_repository.update_user(user.id, updates)
|
|
|
|
|
|
|
|
|
|
# Revoke the reset token
|
|
|
|
|
self.token_repository.revoke_token(token)
|
|
|
|
|
|
|
|
|
|
# Revoke all user's refresh tokens for security
|
|
|
|
|
self.token_repository.revoke_all_user_tokens(user.id, "refresh")
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def request_email_verification(self, email: str) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Generate an email verification token for a user.
|
|
|
|
|
|
|
|
|
|
This method creates a JWT token that can be sent to the user
|
|
|
|
|
to verify their email address. If an email service is configured,
|
|
|
|
|
the email is sent automatically. Otherwise, the token is returned
|
|
|
|
|
for manual handling.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
email: User's email address.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The email verification token (JWT) to be sent via email.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
UserNotFoundError: If email is not registered.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> token = auth_service.request_email_verification("user@example.com")
|
|
|
|
|
>>> # Token is automatically sent via email if service is configured
|
|
|
|
|
"""
|
|
|
|
|
user = self.user_repository.get_user_by_email(email)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError(f"No user found with email {email}")
|
|
|
|
|
|
|
|
|
|
verification_token = self.token_manager.create_email_verification_token(email)
|
|
|
|
|
|
|
|
|
|
# Send email if service is configured
|
|
|
|
|
if self.email_service:
|
|
|
|
|
self.email_service.send_verification_email(email, verification_token)
|
|
|
|
|
|
|
|
|
|
return verification_token
|
|
|
|
|
|
|
|
|
|
def verify_email(self, token: str) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Verify a user's email address using a verification token.
|
|
|
|
|
|
|
|
|
|
This method decodes the JWT token, extracts the email, and marks
|
|
|
|
|
the user's email as verified.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
token: Email verification token (JWT).
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if email was verified successfully.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidTokenError: If verification token is invalid.
|
|
|
|
|
ExpiredTokenError: If verification token has expired.
|
|
|
|
|
UserNotFoundError: If user no longer exists.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> auth_service.verify_email(token)
|
|
|
|
|
True
|
|
|
|
|
"""
|
|
|
|
|
# Decode and validate token
|
|
|
|
|
email = self.token_manager.decode_email_verification_token(token)
|
|
|
|
|
|
|
|
|
|
# Get user
|
|
|
|
|
user = self.user_repository.get_user_by_email(email)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError(f"No user found with email {email}")
|
|
|
|
|
|
|
|
|
|
# Update user's verified status
|
|
|
|
|
updates = UserUpdate(is_verified=True)
|
|
|
|
|
self.user_repository.update_user(user.id, updates)
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def get_current_user(self, access_token: str) -> UserInDB:
|
|
|
|
|
"""
|
|
|
|
|
Retrieve the current user from an access token.
|
|
|
|
|
|
|
|
|
|
This method decodes and validates the JWT access token and
|
|
|
|
|
retrieves the corresponding user from the database.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
access_token: JWT access token.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The user associated with the token.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidTokenError: If access token is invalid.
|
|
|
|
|
ExpiredTokenError: If access token has expired.
|
|
|
|
|
UserNotFoundError: If user no longer exists.
|
|
|
|
|
AccountDisabledError: If user account is disabled.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> user = auth_service.get_current_user(access_token)
|
|
|
|
|
>>> print(user.email)
|
|
|
|
|
"""
|
|
|
|
|
# Decode and validate token
|
|
|
|
|
token_payload = self.token_manager.decode_access_token(access_token)
|
|
|
|
|
|
|
|
|
|
# Get user
|
|
|
|
|
user = self.user_repository.get_user_by_id(token_payload.sub)
|
|
|
|
|
if not user:
|
|
|
|
|
raise UserNotFoundError()
|
|
|
|
|
|
|
|
|
|
if not user.is_active:
|
|
|
|
|
raise AccountDisabledError()
|
|
|
|
|
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
def count_users(self) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Counts the total number of users.
|
|
|
|
|
|
|
|
|
|
This method retrieves and returns the total count of users
|
|
|
|
|
from the user repository. It provides functionality for
|
|
|
|
|
fetching user count stored in the underlying repository of
|
|
|
|
|
users.
|
|
|
|
|
|
|
|
|
|
:return: The total count of users.
|
|
|
|
|
:rtype: int
|
|
|
|
|
"""
|
|
|
|
|
return self.user_repository.count_users()
|
|
|
|
|
|
|
|
|
|
def list_users(self, skip: int = 0, limit: int = 100):
|
|
|
|
|
"""
|
|
|
|
|
Lists users from the user repository with optional pagination.
|
|
|
|
|
|
|
|
|
|
This method retrieves a list of users, allowing optional pagination
|
|
|
|
|
by specifying the number of records to skip and the maximum number
|
|
|
|
|
of users to retrieve.
|
|
|
|
|
|
|
|
|
|
:param skip: The number of users to skip in the result set. Defaults to 0.
|
|
|
|
|
:type skip: int
|
|
|
|
|
:param limit: The maximum number of users to retrieve. Defaults to 100.
|
|
|
|
|
:type limit: int
|
|
|
|
|
:return: A list of users retrieved from the repository.
|
|
|
|
|
:rtype: list
|
|
|
|
|
"""
|
|
|
|
|
return self.user_repository.list_users(skip, limit)
|
|
|
|
|
|