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