Updated README.md
This commit is contained in:
0
src/my_auth/api/__init__.py
Normal file
0
src/my_auth/api/__init__.py
Normal file
379
src/my_auth/api/routes.py
Normal file
379
src/my_auth/api/routes.py
Normal file
@@ -0,0 +1,379 @@
|
||||
"""
|
||||
FastAPI routes for authentication module.
|
||||
|
||||
This module provides ready-to-use FastAPI routes for all authentication
|
||||
operations. Routes are organized in an APIRouter with /auth prefix.
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
|
||||
from ..core.auth import AuthService
|
||||
from ..exceptions import AuthError
|
||||
from ..models.email_verification import (
|
||||
EmailVerificationRequest,
|
||||
PasswordResetRequest,
|
||||
PasswordResetConfirm
|
||||
)
|
||||
from ..models.token import AccessTokenResponse, RefreshTokenRequest
|
||||
from ..models.user import UserCreate, UserResponse
|
||||
|
||||
# OAuth2 scheme for token authentication
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
||||
|
||||
|
||||
def create_auth_router(auth_service: AuthService) -> APIRouter:
|
||||
"""
|
||||
Create and configure the authentication router.
|
||||
|
||||
This factory function creates an APIRouter with all authentication
|
||||
endpoints configured. The router includes automatic exception handling
|
||||
for authentication errors.
|
||||
|
||||
Args:
|
||||
auth_service: Configured authentication service instance.
|
||||
|
||||
Returns:
|
||||
Configured APIRouter ready to be included in a FastAPI app.
|
||||
|
||||
Example:
|
||||
>>> from fastapi import FastAPI
|
||||
>>> from auth_module.api.routes import create_auth_router
|
||||
>>>
|
||||
>>> app = FastAPI()
|
||||
>>> auth_router = create_auth_router(auth_service)
|
||||
>>> app.include_router(auth_router)
|
||||
"""
|
||||
router = APIRouter(prefix="/auth", tags=["authentication"])
|
||||
|
||||
def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> UserResponse:
|
||||
"""
|
||||
Dependency to extract and validate the current user from access token.
|
||||
|
||||
This dependency can be used in any route that requires authentication.
|
||||
It extracts the Bearer token from the Authorization header and validates it.
|
||||
|
||||
Args:
|
||||
token: JWT access token from Authorization header.
|
||||
|
||||
Returns:
|
||||
The current authenticated user (as UserResponse).
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if token is invalid or expired.
|
||||
|
||||
Example:
|
||||
>>> @app.get("/protected")
|
||||
>>> def protected_route(user: UserResponse = Depends(get_current_user)):
|
||||
>>> return {"user_id": user.id}
|
||||
"""
|
||||
try:
|
||||
user = auth_service.get_current_user(token)
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
roles=user.roles,
|
||||
user_settings=user.user_settings,
|
||||
created_at=user.created_at,
|
||||
updated_at=user.updated_at
|
||||
)
|
||||
except AuthError as e:
|
||||
raise HTTPException(
|
||||
status_code=e.status_code,
|
||||
detail=e.message
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/register",
|
||||
response_model=UserResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Register a new user",
|
||||
description="Create a new user account with email, username, and password."
|
||||
)
|
||||
def register(user_data: UserCreate) -> UserResponse:
|
||||
"""
|
||||
Register a new user.
|
||||
|
||||
Creates a new user account with the provided credentials. The password
|
||||
is automatically hashed before storage. Email verification is optional
|
||||
and the account is created with is_verified=False.
|
||||
|
||||
Args:
|
||||
user_data: User registration data including email, username, and password.
|
||||
|
||||
Returns:
|
||||
The created user information (without password).
|
||||
|
||||
Raises:
|
||||
HTTPException: 409 if email already exists.
|
||||
HTTPException: 422 if validation fails (password strength, etc.).
|
||||
"""
|
||||
try:
|
||||
user = auth_service.register(user_data)
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
roles=user.roles,
|
||||
user_settings=user.user_settings,
|
||||
created_at=user.created_at,
|
||||
updated_at=user.updated_at
|
||||
)
|
||||
except AuthError as e:
|
||||
raise HTTPException(
|
||||
status_code=e.status_code,
|
||||
detail=e.message
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/login",
|
||||
response_model=AccessTokenResponse,
|
||||
summary="Login with email and password",
|
||||
description="Authenticate a user and receive access and refresh tokens."
|
||||
)
|
||||
def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> AccessTokenResponse:
|
||||
"""
|
||||
Authenticate a user and generate tokens.
|
||||
|
||||
This endpoint accepts form data with username (which should contain the email)
|
||||
and password. It returns both access and refresh tokens upon successful authentication.
|
||||
|
||||
Note: OAuth2PasswordRequestForm uses 'username' field, but we treat it as email.
|
||||
|
||||
Args:
|
||||
form_data: OAuth2 form with username (email) and password fields.
|
||||
|
||||
Returns:
|
||||
Access token (JWT) and refresh token with token_type="bearer".
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if credentials are invalid.
|
||||
HTTPException: 403 if account is disabled.
|
||||
"""
|
||||
try:
|
||||
# OAuth2PasswordRequestForm uses 'username' but we treat it as email
|
||||
user, tokens = auth_service.login(form_data.username, form_data.password)
|
||||
return tokens
|
||||
except AuthError as e:
|
||||
raise HTTPException(
|
||||
status_code=e.status_code,
|
||||
detail=e.message
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/logout",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Logout user",
|
||||
description="Revoke the refresh token to logout the user."
|
||||
)
|
||||
def logout(request: RefreshTokenRequest) -> None:
|
||||
"""
|
||||
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 remains valid until it expires naturally (30 minutes).
|
||||
|
||||
Args:
|
||||
request: Request body containing the refresh token to revoke.
|
||||
|
||||
Returns:
|
||||
204 No Content on success.
|
||||
"""
|
||||
auth_service.logout(request.refresh_token)
|
||||
return None
|
||||
|
||||
@router.post(
|
||||
"/refresh",
|
||||
response_model=AccessTokenResponse,
|
||||
summary="Refresh access token",
|
||||
description="Exchange a refresh token for new access and refresh tokens."
|
||||
)
|
||||
def refresh_token(request: RefreshTokenRequest) -> AccessTokenResponse:
|
||||
"""
|
||||
Obtain a new access token using a refresh token.
|
||||
|
||||
This endpoint allows clients to obtain a new access token without
|
||||
requiring the user to re-enter their password. The old refresh token
|
||||
is revoked and a new one is issued.
|
||||
|
||||
Args:
|
||||
request: Request body containing the refresh token.
|
||||
|
||||
Returns:
|
||||
New access token and refresh token.
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if refresh token is invalid, expired, or revoked.
|
||||
"""
|
||||
try:
|
||||
tokens = auth_service.refresh_access_token(request.refresh_token)
|
||||
return tokens
|
||||
except AuthError as e:
|
||||
raise HTTPException(
|
||||
status_code=e.status_code,
|
||||
detail=e.message
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/password-reset-request",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Request password reset",
|
||||
description="Generate a password reset token and return it (to be sent via email)."
|
||||
)
|
||||
def request_password_reset(request: PasswordResetRequest) -> dict:
|
||||
"""
|
||||
Request a password reset token.
|
||||
|
||||
This endpoint generates a secure token that can be used to reset the password.
|
||||
In production, this token should be sent via email. For this module, the token
|
||||
is returned in the response so the consuming application can handle email delivery.
|
||||
|
||||
Args:
|
||||
request: Request body containing the email address.
|
||||
|
||||
Returns:
|
||||
Dictionary with the reset token and a message.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 if email is not registered.
|
||||
"""
|
||||
try:
|
||||
token = auth_service.request_password_reset(request.email)
|
||||
return {
|
||||
"message": "Password reset token generated",
|
||||
"token": token
|
||||
}
|
||||
except AuthError as e:
|
||||
raise HTTPException(
|
||||
status_code=e.status_code,
|
||||
detail=e.message
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/password-reset",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Reset password with token",
|
||||
description="Reset user password using a valid reset token."
|
||||
)
|
||||
def reset_password(request: PasswordResetConfirm) -> dict:
|
||||
"""
|
||||
Reset a user's password using a reset token.
|
||||
|
||||
This endpoint validates the reset token and updates the user's password.
|
||||
All existing refresh tokens are revoked for security.
|
||||
|
||||
Args:
|
||||
request: Request body containing the reset token and new password.
|
||||
|
||||
Returns:
|
||||
Success message.
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if token is invalid, expired, or already used.
|
||||
HTTPException: 422 if new password doesn't meet requirements.
|
||||
"""
|
||||
try:
|
||||
auth_service.reset_password(request.token, request.new_password)
|
||||
return {"message": "Password reset successfully"}
|
||||
except AuthError as e:
|
||||
raise HTTPException(
|
||||
status_code=e.status_code,
|
||||
detail=e.message
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/verify-email-request",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Request email verification",
|
||||
description="Generate an email verification token and return it (to be sent via email)."
|
||||
)
|
||||
def request_email_verification(request: EmailVerificationRequest) -> dict:
|
||||
"""
|
||||
Request an email verification token.
|
||||
|
||||
This endpoint generates a JWT token for email verification. In production,
|
||||
this token should be sent via email with a verification link. For this module,
|
||||
the token is returned in the response so the consuming application can handle
|
||||
email delivery.
|
||||
|
||||
Args:
|
||||
request: Request body containing the email address.
|
||||
|
||||
Returns:
|
||||
Dictionary with the verification token and a message.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 if email is not registered.
|
||||
"""
|
||||
try:
|
||||
token = auth_service.request_email_verification(request.email)
|
||||
return {
|
||||
"message": "Email verification token generated",
|
||||
"token": token
|
||||
}
|
||||
except AuthError as e:
|
||||
raise HTTPException(
|
||||
status_code=e.status_code,
|
||||
detail=e.message
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/verify-email",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Verify email with token",
|
||||
description="Verify user email using a verification token from query parameter."
|
||||
)
|
||||
def verify_email(token: str) -> dict:
|
||||
"""
|
||||
Verify a user's email address.
|
||||
|
||||
This endpoint validates the verification token and marks the user's
|
||||
email as verified. It uses a query parameter so it can be easily
|
||||
accessed via a link in an email.
|
||||
|
||||
Args:
|
||||
token: Email verification token (JWT) from query parameter.
|
||||
|
||||
Returns:
|
||||
Success message.
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if token is invalid or expired.
|
||||
"""
|
||||
try:
|
||||
auth_service.verify_email(token)
|
||||
return {"message": "Email verified successfully"}
|
||||
except AuthError as e:
|
||||
raise HTTPException(
|
||||
status_code=e.status_code,
|
||||
detail=e.message
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/me",
|
||||
response_model=UserResponse,
|
||||
summary="Get current user",
|
||||
description="Get information about the currently authenticated user."
|
||||
)
|
||||
def get_me(current_user: Annotated[UserResponse, Depends(get_current_user)]) -> UserResponse:
|
||||
"""
|
||||
Get current authenticated user information.
|
||||
|
||||
This is a protected route that requires a valid access token in the
|
||||
Authorization header (Bearer token).
|
||||
|
||||
Args:
|
||||
current_user: The authenticated user (injected by dependency).
|
||||
|
||||
Returns:
|
||||
Current user information.
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if token is invalid or expired.
|
||||
"""
|
||||
return current_user
|
||||
|
||||
return router
|
||||
@@ -7,439 +7,440 @@ password reset, and email verification.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from .password import PasswordManager
|
||||
from .token import TokenManager
|
||||
from ..exceptions import (
|
||||
InvalidCredentialsError,
|
||||
UserNotFoundError,
|
||||
AccountDisabledError,
|
||||
ExpiredTokenError,
|
||||
InvalidTokenError,
|
||||
RevokedTokenError
|
||||
)
|
||||
from ..models.token import AccessTokenResponse, TokenData
|
||||
from ..models.user import UserCreate, UserInDB, UserUpdate
|
||||
from ..persistence.base import UserRepository, TokenRepository
|
||||
from ..models.user import UserCreate, UserInDB, UserUpdate
|
||||
from ..models.token import AccessTokenResponse, TokenData
|
||||
from ..email.base import EmailService
|
||||
from ..exceptions import (
|
||||
InvalidCredentialsError,
|
||||
UserNotFoundError,
|
||||
AccountDisabledError,
|
||||
ExpiredTokenError,
|
||||
InvalidTokenError,
|
||||
RevokedTokenError
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_repository: UserRepository,
|
||||
token_repository: TokenRepository,
|
||||
password_manager: PasswordManager,
|
||||
token_manager: TokenManager
|
||||
):
|
||||
"""
|
||||
Initialize the authentication service.
|
||||
|
||||
Args:
|
||||
user_repository: Repository for user persistence.
|
||||
token_repository: Repository for token persistence.
|
||||
jwt_secret: Secret key for JWT signing.
|
||||
jwt_algorithm: JWT algorithm (default: HS256).
|
||||
access_token_expire_minutes: Access token validity (default: 30).
|
||||
refresh_token_expire_days: Refresh token validity (default: 7).
|
||||
password_reset_token_expire_minutes: Reset token validity (default: 15).
|
||||
password_hash_rounds: Bcrypt rounds (default: 12).
|
||||
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.
|
||||
"""
|
||||
self.user_repository = user_repository
|
||||
self.token_repository = token_repository
|
||||
self.password_manager = password_manager
|
||||
self.token_manager = token_manager
|
||||
|
||||
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
|
||||
|
||||
def login(self, email: str, password: str) -> tuple[UserInDB, AccessTokenResponse]:
|
||||
"""
|
||||
Authenticate a user and create tokens.
|
||||
|
||||
This method verifies credentials, checks account status, and generates
|
||||
both access and refresh tokens.
|
||||
|
||||
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()
|
||||
|
||||
# Verify password
|
||||
if not self.password_manager.verify_password(password, user.hashed_password):
|
||||
raise InvalidCredentialsError()
|
||||
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
|
||||
|
||||
# Check if account is active
|
||||
if not user.is_active:
|
||||
raise AccountDisabledError()
|
||||
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
|
||||
|
||||
# Create tokens
|
||||
access_token = self.token_manager.create_access_token(user)
|
||||
refresh_token = self.token_manager.create_refresh_token()
|
||||
def login(self, email: str, password: str) -> tuple[UserInDB, AccessTokenResponse]:
|
||||
"""
|
||||
Authenticate a user and create tokens.
|
||||
|
||||
This method verifies credentials, checks account status, and generates
|
||||
both access and refresh tokens.
|
||||
|
||||
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()
|
||||
|
||||
# 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
|
||||
|
||||
# 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.now(),
|
||||
is_revoked=False
|
||||
)
|
||||
self.token_repository.save_token(token_data)
|
||||
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"
|
||||
)
|
||||
|
||||
tokens = AccessTokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=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.
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
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")
|
||||
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
|
||||
|
||||
if token_data.is_revoked:
|
||||
raise RevokedTokenError()
|
||||
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
|
||||
|
||||
if token_data.expires_at < datetime.now():
|
||||
raise ExpiredTokenError()
|
||||
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
|
||||
|
||||
# Get user
|
||||
user = self.user_repository.get_user_by_id(token_data.user_id)
|
||||
if not user:
|
||||
raise UserNotFoundError()
|
||||
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
|
||||
|
||||
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.now(),
|
||||
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.
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
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")
|
||||
>>> # Send token via email service
|
||||
"""
|
||||
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.now(),
|
||||
is_revoked=False
|
||||
)
|
||||
self.token_repository.save_token(token_data)
|
||||
|
||||
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.now():
|
||||
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.
|
||||
|
||||
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")
|
||||
>>> # Send token via email service
|
||||
"""
|
||||
user = self.user_repository.get_user_by_email(email)
|
||||
if not user:
|
||||
raise UserNotFoundError(f"No user found with email {email}")
|
||||
|
||||
return self.token_manager.create_email_verification_token(email)
|
||||
|
||||
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 get_default_sqlite_auth_service(db_path: str, jwt_secret: str) -> AuthService:
|
||||
from my_auth.persistence.sqlite import SQLiteUserRepository
|
||||
from my_auth.persistence.sqlite import SQLiteTokenRepository
|
||||
user_repository = SQLiteUserRepository(db_path=db_path)
|
||||
token_repository = SQLiteTokenRepository(db_path=db_path)
|
||||
password_manager = PasswordManager()
|
||||
token_manager = TokenManager(jwt_secret=jwt_secret)
|
||||
return AuthService(
|
||||
user_repository=user_repository,
|
||||
token_repository=token_repository,
|
||||
password_manager=password_manager,
|
||||
token_manager=token_manager
|
||||
)
|
||||
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
|
||||
0
src/my_auth/email/__init__.py
Normal file
0
src/my_auth/email/__init__.py
Normal file
64
src/my_auth/email/base.py
Normal file
64
src/my_auth/email/base.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Abstract base class for email service.
|
||||
|
||||
This module defines the interface that all email service implementations
|
||||
must follow. It provides methods for sending authentication-related emails.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class EmailService(ABC):
|
||||
"""
|
||||
Abstract base class for email service operations.
|
||||
|
||||
This interface defines all methods required for sending authentication-related
|
||||
emails. Concrete implementations can use different email providers (SMTP,
|
||||
SendGrid, AWS SES, etc.).
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def send_verification_email(self, email: str, token: str) -> None:
|
||||
"""
|
||||
Send an email verification link to the user.
|
||||
|
||||
This method should send an email containing a verification link or token
|
||||
that the user can use to verify their email address.
|
||||
|
||||
Args:
|
||||
email: The recipient's email address.
|
||||
token: The verification token (JWT) to include in the email.
|
||||
|
||||
Raises:
|
||||
Exception: If email sending fails (implementation-specific).
|
||||
|
||||
Example:
|
||||
>>> email_service.send_verification_email(
|
||||
... "user@example.com",
|
||||
... "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
... )
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def send_password_reset_email(self, email: str, token: str) -> None:
|
||||
"""
|
||||
Send a password reset link to the user.
|
||||
|
||||
This method should send an email containing a password reset link or token
|
||||
that the user can use to reset their password.
|
||||
|
||||
Args:
|
||||
email: The recipient's email address.
|
||||
token: The password reset token to include in the email.
|
||||
|
||||
Raises:
|
||||
Exception: If email sending fails (implementation-specific).
|
||||
|
||||
Example:
|
||||
>>> email_service.send_password_reset_email(
|
||||
... "user@example.com",
|
||||
... "a1b2c3d4e5f6..."
|
||||
... )
|
||||
"""
|
||||
pass
|
||||
211
src/my_auth/email/smtp.py
Normal file
211
src/my_auth/email/smtp.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
SMTP email service implementation.
|
||||
|
||||
This module provides an SMTP-based implementation of the email service
|
||||
for sending authentication-related emails.
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from typing import Optional
|
||||
|
||||
from .base import EmailService
|
||||
|
||||
# Default email templates
|
||||
DEFAULT_VERIFICATION_TEMPLATE = """
|
||||
<html>
|
||||
<body>
|
||||
<h2>Email Verification</h2>
|
||||
<p>Please click the link below to verify your email address:</p>
|
||||
<p><a href="{verification_link}">Verify Email</a></p>
|
||||
<p>Or copy and paste this link into your browser:</p>
|
||||
<p>{verification_link}</p>
|
||||
<p>This link will expire in 7 days.</p>
|
||||
<p>If you did not request this verification, please ignore this email.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
DEFAULT_PASSWORD_RESET_TEMPLATE = """
|
||||
<html>
|
||||
<body>
|
||||
<h2>Password Reset</h2>
|
||||
<p>You have requested to reset your password. Please click the link below:</p>
|
||||
<p><a href="{reset_link}">Reset Password</a></p>
|
||||
<p>Or copy and paste this link into your browser:</p>
|
||||
<p>{reset_link}</p>
|
||||
<p>This link will expire in 15 minutes.</p>
|
||||
<p>If you did not request a password reset, please ignore this email.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class SMTPEmailService(EmailService):
|
||||
"""
|
||||
SMTP implementation of EmailService.
|
||||
|
||||
This implementation uses standard SMTP protocol to send emails.
|
||||
It supports both TLS and SSL connections and allows custom HTML
|
||||
templates for email content.
|
||||
|
||||
Attributes:
|
||||
host: SMTP server hostname.
|
||||
port: SMTP server port (587 for TLS, 465 for SSL).
|
||||
username: SMTP authentication username.
|
||||
password: SMTP authentication password.
|
||||
use_tls: Whether to use TLS (default: True).
|
||||
from_email: Email address to use as sender.
|
||||
from_name: Display name for sender (optional).
|
||||
base_url: Base URL for constructing verification/reset links.
|
||||
verification_template: HTML template for verification emails.
|
||||
password_reset_template: HTML template for password reset emails.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int,
|
||||
username: str,
|
||||
password: str,
|
||||
from_email: str,
|
||||
base_url: str,
|
||||
use_tls: bool = True,
|
||||
from_name: Optional[str] = None,
|
||||
verification_template: Optional[str] = None,
|
||||
password_reset_template: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Initialize SMTP email service.
|
||||
|
||||
Args:
|
||||
host: SMTP server hostname (e.g., "smtp.gmail.com").
|
||||
port: SMTP server port (587 for TLS, 465 for SSL).
|
||||
username: SMTP authentication username.
|
||||
password: SMTP authentication password.
|
||||
from_email: Email address to use as sender.
|
||||
base_url: Base URL for your application (e.g., "https://myapp.com").
|
||||
use_tls: Whether to use TLS encryption (default: True).
|
||||
from_name: Display name for sender (default: None).
|
||||
verification_template: Custom HTML template for verification emails
|
||||
(must include {verification_link} placeholder).
|
||||
password_reset_template: Custom HTML template for reset emails
|
||||
(must include {reset_link} placeholder).
|
||||
|
||||
Example:
|
||||
>>> email_service = SMTPEmailService(
|
||||
... host="smtp.gmail.com",
|
||||
... port=587,
|
||||
... username="noreply@myapp.com",
|
||||
... password="app_password",
|
||||
... from_email="noreply@myapp.com",
|
||||
... base_url="https://myapp.com",
|
||||
... from_name="My Application"
|
||||
... )
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.use_tls = use_tls
|
||||
self.from_email = from_email
|
||||
self.from_name = from_name
|
||||
self.base_url = base_url.rstrip('/')
|
||||
|
||||
# Use custom templates or defaults
|
||||
self.verification_template = verification_template or DEFAULT_VERIFICATION_TEMPLATE
|
||||
self.password_reset_template = password_reset_template or DEFAULT_PASSWORD_RESET_TEMPLATE
|
||||
|
||||
def _send_email(self, to_email: str, subject: str, html_content: str) -> None:
|
||||
"""
|
||||
Send an email via SMTP.
|
||||
|
||||
Internal method that handles the actual SMTP connection and sending.
|
||||
|
||||
Args:
|
||||
to_email: Recipient email address.
|
||||
subject: Email subject line.
|
||||
html_content: HTML content of the email.
|
||||
|
||||
Raises:
|
||||
smtplib.SMTPException: If email sending fails.
|
||||
"""
|
||||
# Create message
|
||||
message = MIMEMultipart("alternative")
|
||||
message["Subject"] = subject
|
||||
message["From"] = f"{self.from_name} <{self.from_email}>" if self.from_name else self.from_email
|
||||
message["To"] = to_email
|
||||
|
||||
# Attach HTML content
|
||||
html_part = MIMEText(html_content, "html")
|
||||
message.attach(html_part)
|
||||
|
||||
# Send email
|
||||
if self.use_tls:
|
||||
with smtplib.SMTP(self.host, self.port) as server:
|
||||
server.starttls()
|
||||
server.login(self.username, self.password)
|
||||
server.send_message(message)
|
||||
else:
|
||||
with smtplib.SMTP_SSL(self.host, self.port) as server:
|
||||
server.login(self.username, self.password)
|
||||
server.send_message(message)
|
||||
|
||||
def send_verification_email(self, email: str, token: str) -> None:
|
||||
"""
|
||||
Send an email verification link to the user.
|
||||
|
||||
Constructs a verification link using the base_url and token,
|
||||
then sends an email using the configured verification template.
|
||||
|
||||
Args:
|
||||
email: The recipient's email address.
|
||||
token: The verification token (JWT).
|
||||
|
||||
Raises:
|
||||
smtplib.SMTPException: If email sending fails.
|
||||
|
||||
Example:
|
||||
>>> email_service.send_verification_email(
|
||||
... "user@example.com",
|
||||
... "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
... )
|
||||
"""
|
||||
verification_link = f"{self.base_url}/auth/verify-email?token={token}"
|
||||
html_content = self.verification_template.format(verification_link=verification_link)
|
||||
|
||||
self._send_email(
|
||||
to_email=email,
|
||||
subject="Verify Your Email Address",
|
||||
html_content=html_content
|
||||
)
|
||||
|
||||
def send_password_reset_email(self, email: str, token: str) -> None:
|
||||
"""
|
||||
Send a password reset link to the user.
|
||||
|
||||
Constructs a password reset link using the base_url and token,
|
||||
then sends an email using the configured password reset template.
|
||||
|
||||
Args:
|
||||
email: The recipient's email address.
|
||||
token: The password reset token.
|
||||
|
||||
Raises:
|
||||
smtplib.SMTPException: If email sending fails.
|
||||
|
||||
Example:
|
||||
>>> email_service.send_password_reset_email(
|
||||
... "user@example.com",
|
||||
... "a1b2c3d4e5f6..."
|
||||
... )
|
||||
"""
|
||||
reset_link = f"{self.base_url}/reset-password?token={token}"
|
||||
html_content = self.password_reset_template.format(reset_link=reset_link)
|
||||
|
||||
self._send_email(
|
||||
to_email=email,
|
||||
subject="Reset Your Password",
|
||||
html_content=html_content
|
||||
)
|
||||
@@ -162,7 +162,7 @@ class SQLiteUserRepository(UserRepository):
|
||||
raise UserAlreadyExistsError(f"User with email {user_data.email} already exists")
|
||||
|
||||
user_id = str(uuid4())
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now()
|
||||
|
||||
user = UserInDB(
|
||||
id=user_id,
|
||||
@@ -315,7 +315,7 @@ class SQLiteUserRepository(UserRepository):
|
||||
|
||||
# Always update the updated_at timestamp
|
||||
update_fields.append("updated_at = ?")
|
||||
update_values.append(datetime.utcnow().isoformat())
|
||||
update_values.append(datetime.now().isoformat())
|
||||
|
||||
# Add user_id for WHERE clause
|
||||
update_values.append(user_id)
|
||||
@@ -574,7 +574,7 @@ class SQLiteTokenRepository(TokenRepository):
|
||||
Returns:
|
||||
Number of tokens deleted.
|
||||
"""
|
||||
now = datetime.utcnow().isoformat()
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
Reference in New Issue
Block a user