2026-02-17 11:57:24 +05:30
|
|
|
from fastapi import APIRouter, Depends, Request, Response, status, HTTPException
|
2026-02-16 15:32:05 +05:30
|
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
2026-01-17 14:18:00 +05:30
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
from app.config.database import get_db
|
2026-02-17 11:57:24 +05:30
|
|
|
from app.config.settings import settings
|
2026-01-17 14:18:00 +05:30
|
|
|
from app.controllers.auth.auth_controller import AuthController
|
|
|
|
|
from app.schemas.auth.auth_schema import (
|
|
|
|
|
UserSignup,
|
|
|
|
|
UserSignin,
|
|
|
|
|
TokenResponse,
|
|
|
|
|
UserResponse,
|
|
|
|
|
UserUpdate,
|
|
|
|
|
ResetPassword,
|
|
|
|
|
RefreshTokenRequest,
|
|
|
|
|
ForgotPasswordRequest,
|
|
|
|
|
VerifyOTPRequest,
|
|
|
|
|
ResetPasswordWithOTP,
|
|
|
|
|
)
|
2026-02-16 15:32:05 +05:30
|
|
|
from app.schemas.auth.language_schema import UpdateLanguageRequest
|
|
|
|
|
from app.middleware.auth_middleware import get_current_user, security_scheme
|
2026-08-31 20:04:12 -04:00
|
|
|
from app.middleware.rate_limit import (
|
|
|
|
|
SIGNIN_LIMIT,
|
|
|
|
|
SIGNUP_LIMIT,
|
|
|
|
|
FORGOT_PASSWORD_LIMIT,
|
|
|
|
|
VERIFY_OTP_LIMIT,
|
|
|
|
|
RESET_PASSWORD_OTP_LIMIT,
|
|
|
|
|
)
|
2026-01-17 14:18:00 +05:30
|
|
|
from app.models.auth.user_model import User
|
2026-08-31 20:04:12 -04:00
|
|
|
from app.helper.helpers import get_client_ip
|
2026-01-17 14:18:00 +05:30
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
2026-02-17 11:57:24 +05:30
|
|
|
def _cookie_kwargs():
|
|
|
|
|
kwargs = dict(httponly=True, samesite="lax", secure=settings.COOKIE_SECURE, path="/")
|
|
|
|
|
if settings.COOKIE_DOMAIN:
|
|
|
|
|
kwargs["domain"] = settings.COOKIE_DOMAIN
|
|
|
|
|
return kwargs
|
|
|
|
|
|
2026-08-31 20:04:12 -04:00
|
|
|
@router.post(
|
|
|
|
|
"/signup",
|
|
|
|
|
response_model=UserResponse,
|
|
|
|
|
status_code=status.HTTP_201_CREATED,
|
|
|
|
|
dependencies=[Depends(SIGNUP_LIMIT)],
|
|
|
|
|
)
|
2026-01-17 14:18:00 +05:30
|
|
|
def signup(
|
|
|
|
|
user_data: UserSignup,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
):
|
2026-08-31 20:04:12 -04:00
|
|
|
"""Self-service signup, off unless ALLOW_PUBLIC_SIGNUP is set.
|
2026-01-17 14:18:00 +05:30
|
|
|
|
2026-08-31 20:04:12 -04:00
|
|
|
The workspace to join is no longer taken from the X-Tenant-ID request header.
|
|
|
|
|
That value was attacker-controlled, so anyone who knew a workspace id could
|
|
|
|
|
register into it; and no legitimate client ever sent it, so every real signup
|
|
|
|
|
produced a tenant-less account that the platform treated as a superadmin.
|
|
|
|
|
"""
|
|
|
|
|
if not settings.ALLOW_PUBLIC_SIGNUP:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
|
|
|
detail="Public signup is disabled. Ask a workspace administrator for an account.",
|
|
|
|
|
)
|
|
|
|
|
return AuthController.signup(db, user_data, tenant_id=None)
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/signin",
|
|
|
|
|
response_model=TokenResponse,
|
|
|
|
|
dependencies=[Depends(SIGNIN_LIMIT)],
|
|
|
|
|
)
|
|
|
|
|
def signin(
|
|
|
|
|
signin_data: UserSignin,
|
|
|
|
|
request: Request,
|
|
|
|
|
response: Response,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
result = AuthController.signin(
|
|
|
|
|
db,
|
|
|
|
|
signin_data,
|
|
|
|
|
user_agent=request.headers.get("user-agent"),
|
|
|
|
|
ip_address=get_client_ip(request),
|
|
|
|
|
)
|
2026-02-17 11:57:24 +05:30
|
|
|
cookie_kw = _cookie_kwargs()
|
|
|
|
|
access_max_age = settings.ACCESS_TOKEN_EXPIRES if signin_data.remember_me else None
|
|
|
|
|
refresh_max_age = settings.REFRESH_TOKEN_EXPIRES if signin_data.remember_me else None
|
|
|
|
|
response.set_cookie(key="access_token", value=result["access_token"], max_age=access_max_age, **cookie_kw)
|
|
|
|
|
response.set_cookie(key="refresh_token", value=result["refresh_token"], max_age=refresh_max_age, **cookie_kw)
|
|
|
|
|
return result
|
2026-01-17 14:18:00 +05:30
|
|
|
|
|
|
|
|
@router.post("/refresh", response_model=TokenResponse)
|
2026-02-17 11:57:24 +05:30
|
|
|
def refresh_token(
|
|
|
|
|
token_data: RefreshTokenRequest,
|
|
|
|
|
request: Request,
|
|
|
|
|
response: Response,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
refresh_tok = token_data.refresh_token or request.cookies.get("refresh_token")
|
|
|
|
|
if not refresh_tok:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing refresh token")
|
|
|
|
|
result = AuthController.refresh_token_raw(db, refresh_tok)
|
|
|
|
|
cookie_kw = _cookie_kwargs()
|
|
|
|
|
response.set_cookie(key="access_token", value=result["access_token"], **cookie_kw)
|
|
|
|
|
response.set_cookie(key="refresh_token", value=result["refresh_token"], **cookie_kw)
|
|
|
|
|
return result
|
2026-01-17 14:18:00 +05:30
|
|
|
|
|
|
|
|
@router.get("/me", response_model=UserResponse)
|
|
|
|
|
def get_me(
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
return AuthController.me(db, current_user)
|
|
|
|
|
|
|
|
|
|
@router.put("/update/{user_id}", response_model=UserResponse)
|
|
|
|
|
def update_user(
|
|
|
|
|
user_id: uuid.UUID,
|
|
|
|
|
user_data: UserUpdate,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
return AuthController.update_user(db, user_id, user_data, current_user)
|
|
|
|
|
|
|
|
|
|
@router.patch("/update/{user_id}/language", response_model=UserResponse)
|
|
|
|
|
def update_language(
|
|
|
|
|
user_id: uuid.UUID,
|
2026-02-16 15:32:05 +05:30
|
|
|
language_data: UpdateLanguageRequest,
|
2026-01-17 14:18:00 +05:30
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
2026-02-16 15:32:05 +05:30
|
|
|
user_update = UserUpdate(preferred_language=language_data.preferred_language.value)
|
2026-01-17 14:18:00 +05:30
|
|
|
return AuthController.update_user(db, user_id, user_update, current_user)
|
|
|
|
|
|
|
|
|
|
@router.post("/reset-password")
|
|
|
|
|
def reset_password(
|
|
|
|
|
password_data: ResetPassword,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
return AuthController.reset_password(db, current_user, password_data)
|
|
|
|
|
|
|
|
|
|
@router.post("/logout")
|
2026-02-16 15:32:05 +05:30
|
|
|
def logout(
|
|
|
|
|
request: Request,
|
2026-02-17 11:57:24 +05:30
|
|
|
response: Response,
|
2026-08-31 20:04:12 -04:00
|
|
|
db: Session = Depends(get_db),
|
2026-02-16 15:32:05 +05:30
|
|
|
credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
token = credentials.credentials if credentials else request.cookies.get("access_token")
|
2026-08-31 20:04:12 -04:00
|
|
|
refresh_tok = request.cookies.get("refresh_token")
|
|
|
|
|
result = AuthController.logout(current_user, token, refresh_tok, db=db)
|
2026-02-17 11:57:24 +05:30
|
|
|
cookie_kw = _cookie_kwargs()
|
|
|
|
|
response.delete_cookie(key="access_token", **cookie_kw)
|
|
|
|
|
response.delete_cookie(key="refresh_token", **cookie_kw)
|
|
|
|
|
return result
|
2026-01-17 14:18:00 +05:30
|
|
|
|
2026-08-31 20:04:12 -04:00
|
|
|
@router.post("/forgot-password", dependencies=[Depends(FORGOT_PASSWORD_LIMIT)])
|
2026-01-17 14:18:00 +05:30
|
|
|
def forgot_password(request: ForgotPasswordRequest, db: Session = Depends(get_db)):
|
|
|
|
|
return AuthController.forgot_password(db, request)
|
|
|
|
|
|
2026-08-31 20:04:12 -04:00
|
|
|
@router.post("/verify-otp", dependencies=[Depends(VERIFY_OTP_LIMIT)])
|
2026-01-17 14:18:00 +05:30
|
|
|
def verify_otp(request: VerifyOTPRequest, db: Session = Depends(get_db)):
|
|
|
|
|
return AuthController.verify_otp(db, request)
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 20:04:12 -04:00
|
|
|
@router.post(
|
|
|
|
|
"/reset-password-otp", dependencies=[Depends(RESET_PASSWORD_OTP_LIMIT)]
|
|
|
|
|
)
|
2026-01-17 14:18:00 +05:30
|
|
|
def reset_password_with_otp(
|
|
|
|
|
request: ResetPasswordWithOTP, db: Session = Depends(get_db)
|
|
|
|
|
):
|
2026-08-31 20:04:12 -04:00
|
|
|
return AuthController.reset_password_with_otp(db, request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/sessions")
|
|
|
|
|
def list_sessions(
|
|
|
|
|
request: Request,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Where this account is currently signed in, most recent first."""
|
|
|
|
|
return AuthController.list_sessions(
|
|
|
|
|
db, current_user, request.cookies.get("refresh_token")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/sessions/{session_id}")
|
|
|
|
|
def end_session(
|
|
|
|
|
session_id: uuid.UUID,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""End one session. Only your own."""
|
|
|
|
|
return AuthController.end_session(db, current_user, session_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/sessions/revoke-others")
|
|
|
|
|
def end_other_sessions(
|
|
|
|
|
request: Request,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Sign out everywhere else, sparing the session doing the asking."""
|
|
|
|
|
return AuthController.end_other_sessions(
|
|
|
|
|
db, current_user, request.cookies.get("refresh_token")
|
|
|
|
|
)
|