updates
This commit is contained in:
@@ -7,7 +7,7 @@ import uuid
|
||||
import os
|
||||
from ..config.database import get_db
|
||||
from ..services.auth_service import auth_service
|
||||
from ..schemas.auth import RegisterRequest, LoginRequest, RefreshTokenRequest, ForgotPasswordRequest, ResetPasswordRequest, AuthResponse, TokenResponse, MessageResponse, MFAInitResponse, EnableMFARequest, VerifyMFARequest, MFAStatusResponse
|
||||
from ..schemas.auth import RegisterRequest, LoginRequest, RefreshTokenRequest, ForgotPasswordRequest, ResetPasswordRequest, AuthResponse, TokenResponse, MessageResponse, MFAInitResponse, EnableMFARequest, VerifyMFARequest, MFAStatusResponse, UpdateProfileRequest
|
||||
from ..middleware.auth import get_current_user
|
||||
from ..models.user import User
|
||||
from ..services.audit_service import audit_service
|
||||
@@ -22,16 +22,15 @@ AUTH_RATE_LIMIT = "5/minute" # 5 attempts per minute per IP
|
||||
PASSWORD_RESET_LIMIT = "3/hour" # 3 password reset requests per hour per IP
|
||||
LOGIN_RATE_LIMIT = "10/minute" # 10 login attempts per minute per IP
|
||||
|
||||
# Initialize limiter - will be set from app state
|
||||
limiter = None
|
||||
|
||||
def get_limiter(request: Request) -> Limiter:
|
||||
"""Get limiter instance from app state."""
|
||||
return request.app.state.limiter if hasattr(request.app.state, 'limiter') else None
|
||||
|
||||
def apply_rate_limit(func, limit_value: str):
|
||||
"""Helper to apply rate limiting decorator if limiter is available."""
|
||||
def decorator(*args, **kwargs):
|
||||
# This will be applied at runtime when route is called
|
||||
return func(*args, **kwargs)
|
||||
return decorator
|
||||
global limiter
|
||||
if hasattr(request.app.state, 'limiter'):
|
||||
limiter = request.app.state.limiter
|
||||
return limiter
|
||||
|
||||
def get_base_url(request: Request) -> str:
|
||||
return os.getenv('SERVER_URL') or f'http://{request.headers.get('host', 'localhost:8000')}'
|
||||
@@ -52,6 +51,7 @@ async def register(
|
||||
response: Response,
|
||||
db: Session=Depends(get_db)
|
||||
):
|
||||
# Rate limiting is handled by middleware, but we can add additional checks here if needed
|
||||
client_ip = request.client.host if request.client else None
|
||||
user_agent = request.headers.get('User-Agent')
|
||||
request_id = getattr(request.state, 'request_id', None)
|
||||
@@ -59,14 +59,28 @@ async def register(
|
||||
try:
|
||||
result = await auth_service.register(db=db, name=register_request.name, email=register_request.email, password=register_request.password, phone=register_request.phone)
|
||||
from ..config.settings import settings
|
||||
max_age = 7 * 24 * 60 * 60 # 7 days for registration
|
||||
# Use secure cookies in production (HTTPS required)
|
||||
# Set access token in httpOnly cookie for security
|
||||
# Use 'lax' in development for cross-origin support, 'strict' in production
|
||||
samesite_value = 'strict' if settings.is_production else 'lax'
|
||||
response.set_cookie(
|
||||
key='accessToken',
|
||||
value=result['token'],
|
||||
httponly=True,
|
||||
secure=settings.is_production, # Secure flag enabled in production
|
||||
samesite=samesite_value,
|
||||
max_age=max_age,
|
||||
path='/'
|
||||
)
|
||||
# Set refresh token in httpOnly cookie
|
||||
response.set_cookie(
|
||||
key='refreshToken',
|
||||
value=result['refreshToken'],
|
||||
httponly=True,
|
||||
secure=settings.is_production, # Secure flag enabled in production
|
||||
samesite='strict',
|
||||
max_age=7 * 24 * 60 * 60,
|
||||
samesite=samesite_value,
|
||||
max_age=max_age,
|
||||
path='/'
|
||||
)
|
||||
|
||||
@@ -83,7 +97,8 @@ async def register(
|
||||
status='success'
|
||||
)
|
||||
|
||||
return {'status': 'success', 'message': 'Registration successful', 'data': {'token': result['token'], 'user': result['user']}}
|
||||
# Return user data but NOT the token (it's in httpOnly cookie now)
|
||||
return {'status': 'success', 'message': 'Registration successful', 'data': {'user': result['user']}}
|
||||
except ValueError as e:
|
||||
error_message = str(e)
|
||||
# Log failed registration attempt
|
||||
@@ -132,12 +147,25 @@ async def login(
|
||||
from ..config.settings import settings
|
||||
max_age = 7 * 24 * 60 * 60 if login_request.rememberMe else 1 * 24 * 60 * 60
|
||||
# Use secure cookies in production (HTTPS required)
|
||||
# Set access token in httpOnly cookie for security
|
||||
# Use 'lax' in development for cross-origin support, 'strict' in production
|
||||
samesite_value = 'strict' if settings.is_production else 'lax'
|
||||
response.set_cookie(
|
||||
key='accessToken',
|
||||
value=result['token'],
|
||||
httponly=True,
|
||||
secure=settings.is_production, # Secure flag enabled in production
|
||||
samesite=samesite_value,
|
||||
max_age=max_age,
|
||||
path='/'
|
||||
)
|
||||
# Set refresh token in httpOnly cookie
|
||||
response.set_cookie(
|
||||
key='refreshToken',
|
||||
value=result['refreshToken'],
|
||||
httponly=True,
|
||||
secure=settings.is_production, # Secure flag enabled in production
|
||||
samesite='strict',
|
||||
samesite=samesite_value,
|
||||
max_age=max_age,
|
||||
path='/'
|
||||
)
|
||||
@@ -155,7 +183,8 @@ async def login(
|
||||
status='success'
|
||||
)
|
||||
|
||||
return {'status': 'success', 'data': {'token': result['token'], 'user': result['user']}}
|
||||
# Return user data but NOT the token (it's in httpOnly cookie now)
|
||||
return {'status': 'success', 'data': {'user': result['user']}}
|
||||
except ValueError as e:
|
||||
error_message = str(e)
|
||||
status_code = status.HTTP_401_UNAUTHORIZED if 'Invalid email or password' in error_message or 'Invalid MFA token' in error_message else status.HTTP_400_BAD_REQUEST
|
||||
@@ -176,12 +205,32 @@ async def login(
|
||||
return JSONResponse(status_code=status_code, content={'status': 'error', 'message': error_message})
|
||||
|
||||
@router.post('/refresh-token', response_model=TokenResponse)
|
||||
async def refresh_token(refreshToken: str=Cookie(None), db: Session=Depends(get_db)):
|
||||
async def refresh_token(
|
||||
request: Request,
|
||||
response: Response,
|
||||
refreshToken: str=Cookie(None),
|
||||
db: Session=Depends(get_db)
|
||||
):
|
||||
if not refreshToken:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Refresh token not found')
|
||||
try:
|
||||
result = await auth_service.refresh_access_token(db, refreshToken)
|
||||
return result
|
||||
from ..config.settings import settings
|
||||
# Set new access token in httpOnly cookie
|
||||
# Use 'lax' in development for cross-origin support, 'strict' in production
|
||||
samesite_value = 'strict' if settings.is_production else 'lax'
|
||||
max_age = 7 * 24 * 60 * 60 # 7 days
|
||||
response.set_cookie(
|
||||
key='accessToken',
|
||||
value=result['token'],
|
||||
httponly=True,
|
||||
secure=settings.is_production,
|
||||
samesite=samesite_value,
|
||||
max_age=max_age,
|
||||
path='/'
|
||||
)
|
||||
# Return user data but NOT the token (it's in httpOnly cookie now)
|
||||
return {'status': 'success', 'data': {'user': result.get('user')}}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
|
||||
|
||||
@@ -199,7 +248,13 @@ async def logout(
|
||||
|
||||
if refreshToken:
|
||||
await auth_service.logout(db, refreshToken)
|
||||
response.delete_cookie(key='refreshToken', path='/')
|
||||
|
||||
# Delete both access and refresh token cookies
|
||||
from ..config.settings import settings
|
||||
# Use 'lax' in development for cross-origin support, 'strict' in production
|
||||
samesite_value = 'strict' if settings.is_production else 'lax'
|
||||
response.delete_cookie(key='refreshToken', path='/', secure=settings.is_production, samesite=samesite_value)
|
||||
response.delete_cookie(key='accessToken', path='/', secure=settings.is_production, samesite=samesite_value)
|
||||
|
||||
# Log logout
|
||||
await audit_service.log_action(
|
||||
@@ -227,9 +282,18 @@ async def get_profile(current_user: User=Depends(get_current_user), db: Session=
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
@router.put('/profile')
|
||||
async def update_profile(profile_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
async def update_profile(profile_data: UpdateProfileRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
user = await auth_service.update_profile(db=db, user_id=current_user.id, full_name=profile_data.get('full_name'), email=profile_data.get('email'), phone_number=profile_data.get('phone_number'), password=profile_data.get('password'), current_password=profile_data.get('currentPassword'), currency=profile_data.get('currency'))
|
||||
user = await auth_service.update_profile(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
full_name=profile_data.full_name,
|
||||
email=profile_data.email,
|
||||
phone_number=profile_data.phone_number,
|
||||
password=profile_data.password,
|
||||
current_password=profile_data.currentPassword,
|
||||
currency=profile_data.currency
|
||||
)
|
||||
return {'status': 'success', 'message': 'Profile updated successfully', 'data': {'user': user}}
|
||||
except ValueError as e:
|
||||
error_message = str(e)
|
||||
|
||||
Reference in New Issue
Block a user