File size: 5,940 Bytes
d74863e
 
 
d03ed31
d74863e
 
 
 
 
 
 
 
6405808
d74863e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6405808
d74863e
 
 
 
 
 
 
 
 
 
6405808
d74863e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6405808
d74863e
 
6405808
d74863e
6405808
 
 
d74863e
6405808
 
 
 
 
 
 
d74863e
 
 
 
 
 
 
 
6405808
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d74863e
 
 
 
 
6405808
d74863e
 
 
 
 
6405808
d74863e
 
6405808
d74863e
6405808
 
 
 
 
 
d74863e
6405808
 
d74863e
 
6405808
 
 
d74863e
 
6405808
 
 
 
 
 
 
 
 
d74863e
 
 
 
 
 
 
 
 
6405808
d74863e
 
6405808
d74863e
 
 
 
 
 
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
176
177
178
179
180
181
182
183
184
185
186
187
188
"""
Authentication API endpoints for user signup and login.
"""
from datetime import datetime
from datetime import timedelta
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel, EmailStr, Field
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession

from src.db.firebase import get_firebase_db
from src.db.models import User
from src.auth.security import hash_password, verify_password, create_access_token
from src.utils.logger import setup_logger
from src.utils.config import settings

logger = setup_logger(__name__)

router = APIRouter(prefix="/auth", tags=["Authentication"])


# Request/Response Models
class SignupRequest(BaseModel):
    """Request model for user signup."""

    email: EmailStr = Field(..., description="User email address")
    username: str = Field(..., min_length=3, max_length=50, description="Username")
    password: str = Field(..., min_length=6, description="Password (min 6 characters)")
    age: Optional[int] = Field(None, ge=0, le=120, description="User age")
    gender: Optional[str] = Field(None, max_length=20, description="User gender")

    class Config:
        json_schema_extra = {
            "example": {
                "email": "student@example.com",
                "username": "Student123",
                "password": "secure_password",
            }
        }


class UserResponse(BaseModel):
    """Response model for user data (without password)."""

    id: str
    email: str
    username: str
    role: str
    age: Optional[int] = None
    gender: Optional[str] = None
    created_at: str

    class Config:
        json_schema_extra = {
            "example": {
                "id": "abc-123",
                "email": "student@example.com",
                "username": "Student123",
                "role": "user",
                "created_at": "2024-01-27T05:00:00",
            }
        }


class TokenResponse(BaseModel):
    """Response model for login token."""

    access_token: str = Field(..., description="JWT access token")
    token_type: str = Field(default="bearer", description="Token type")
    expires_in: int = Field(..., description="Token expiration time in minutes")

    class Config:
        json_schema_extra = {
            "example": {
                "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
                "token_type": "bearer",
                "expires_in": 60,
            }
        }


@router.post(
    "/signup", response_model=UserResponse, status_code=status.HTTP_201_CREATED
)
async def signup(
    signup_data: SignupRequest
):
    """
    Register a new user using Firestore.
    """
    db = get_firebase_db()
    if db is None:
         raise HTTPException(status_code=500, detail="Firebase not configured")

    # Check if email or username already exists
    users_ref = db.collection("users")
    
    email_check = users_ref.where("email", "==", signup_data.email).limit(1).stream()
    username_check = users_ref.where("username", "==", signup_data.username).limit(1).stream()
    
    if next(email_check, None) or next(username_check, None):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Email or Username already registered",
        )

    # Create new user with hashed password
    hashed_password_value = hash_password(signup_data.password)

    user_dict = {
        "email": signup_data.email,
        "username": signup_data.username,
        "password_hash": hashed_password_value,
        "role": "user",
        "age": signup_data.age,
        "gender": signup_data.gender,
        "created_at": datetime.utcnow()
    }

    _, new_user_ref = users_ref.add(user_dict)
    
    logger.info(f"New user registered in Firestore: {signup_data.email}")

    return UserResponse(
        id=new_user_ref.id,
        email=signup_data.email,
        username=signup_data.username,
        role="user",
        age=signup_data.age,
        gender=signup_data.gender,
        created_at=str(user_dict["created_at"]),
    )


@router.post("/login", response_model=TokenResponse)
async def login(
    form_data: OAuth2PasswordRequestForm = Depends()
):
    """
    Authenticate user and return JWT access token using Firestore.
    """
    db = get_firebase_db()
    if db is None:
         raise HTTPException(status_code=500, detail="Firebase not configured")
         
    users_ref = db.collection("users")
    
    # Find user by username
    query = users_ref.where("username", "==", form_data.username).limit(1).stream()
    user_doc = next(query, None)

    # If not found by username, try finding by email
    if not user_doc:
        query = users_ref.where("email", "==", form_data.username).limit(1).stream()
        user_doc = next(query, None)

    # Verify user exists and password is correct
    if not user_doc:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    user_data = user_doc.to_dict()
    if not verify_password(form_data.password, user_data["password_hash"]):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

    # Create access token
    access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
    access_token = create_access_token(
        data={"sub": user_data["username"]}, expires_delta=access_token_expires
    )

    logger.info(f"User logged in from Firestore: {user_data['username']}")

    return TokenResponse(
        access_token=access_token,
        token_type="bearer",
        expires_in=settings.access_token_expire_minutes,
    )