Spaces:
Running
Running
File size: 5,301 Bytes
7ffe51d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
"""Authentication service for user signup and signin."""
from sqlmodel import Session, select
from fastapi import HTTPException, status
from src.models.user import User
from src.schemas.auth import SignupRequest, SigninRequest, SignupResponse, TokenResponse, UserProfile
from src.core.security import hash_password, verify_password, create_jwt_token
from src.core.config import settings
from datetime import datetime
import re
class AuthService:
"""Service for handling authentication operations."""
def __init__(self, db: Session):
self.db = db
def signup(self, signup_data: SignupRequest) -> SignupResponse:
"""
Create a new user account.
Args:
signup_data: User signup information
Returns:
SignupResponse with created user details
Raises:
HTTPException: 409 if email already exists
HTTPException: 400 if validation fails
"""
# Validate email format (RFC 5322)
if not self._validate_email(signup_data.email):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid email format",
)
# Validate password strength
password_errors = self._validate_password(signup_data.password)
if password_errors:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password does not meet requirements",
headers={"X-Password-Errors": ", ".join(password_errors)}
)
# Check if email already exists
existing_user = self.db.exec(
select(User).where(User.email == signup_data.email)
).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Email already registered",
)
# Hash password
password_hash = hash_password(signup_data.password)
# Create user
user = User(
email=signup_data.email,
name=signup_data.name,
password_hash=password_hash,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
self.db.add(user)
self.db.commit()
self.db.refresh(user)
return SignupResponse(
id=user.id,
email=user.email,
name=user.name,
created_at=user.created_at,
)
def signin(self, signin_data: SigninRequest) -> TokenResponse:
"""
Authenticate user and issue JWT token.
Args:
signin_data: User signin credentials
Returns:
TokenResponse with JWT token and user profile
Raises:
HTTPException: 401 if credentials are invalid
"""
# Find user by email
user = self.db.exec(
select(User).where(User.email == signin_data.email)
).first()
# Verify password
if not user or not verify_password(signin_data.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
)
# Create JWT token
token = create_jwt_token(
user_id=user.id,
email=user.email,
secret=settings.BETTER_AUTH_SECRET,
expiration_days=settings.JWT_EXPIRATION_DAYS,
)
# Calculate expiration in seconds (7 days = 604800 seconds)
expires_in = settings.JWT_EXPIRATION_DAYS * 24 * 60 * 60
return TokenResponse(
access_token=token,
token_type="bearer",
expires_in=expires_in,
user=UserProfile(
id=user.id,
email=user.email,
name=user.name,
created_at=user.created_at,
),
)
def _validate_email(self, email: str) -> bool:
"""
Validate email format (RFC 5322).
Args:
email: Email address to validate
Returns:
True if valid, False otherwise
"""
# Simplified RFC 5322 email validation
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
def _validate_password(self, password: str) -> list[str]:
"""
Validate password strength.
Requirements:
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
Args:
password: Password to validate
Returns:
List of validation error messages (empty if valid)
"""
errors = []
if len(password) < 8:
errors.append("Password must be at least 8 characters")
if not re.search(r'[A-Z]', password):
errors.append("Password must contain at least one uppercase letter")
if not re.search(r'[a-z]', password):
errors.append("Password must contain at least one lowercase letter")
if not re.search(r'\d', password):
errors.append("Password must contain at least one number")
return errors
|