updates
This commit is contained in:
@@ -10,8 +10,29 @@ from ..services.auth_service import auth_service
|
||||
from ..schemas.auth import RegisterRequest, LoginRequest, RefreshTokenRequest, ForgotPasswordRequest, ResetPasswordRequest, AuthResponse, TokenResponse, MessageResponse, MFAInitResponse, EnableMFARequest, VerifyMFARequest, MFAStatusResponse
|
||||
from ..middleware.auth import get_current_user
|
||||
from ..models.user import User
|
||||
from ..services.audit_service import audit_service
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
|
||||
router = APIRouter(prefix='/auth', tags=['auth'])
|
||||
|
||||
# Stricter rate limits for authentication endpoints
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def get_base_url(request: Request) -> str:
|
||||
return os.getenv('SERVER_URL') or f'http://{request.headers.get('host', 'localhost:8000')}'
|
||||
|
||||
@@ -25,27 +46,133 @@ def normalize_image_url(image_url: str, base_url: str) -> str:
|
||||
return f'{base_url}/{image_url}'
|
||||
|
||||
@router.post('/register', status_code=status.HTTP_201_CREATED)
|
||||
async def register(request: RegisterRequest, response: Response, db: Session=Depends(get_db)):
|
||||
async def register(
|
||||
request: Request,
|
||||
register_request: RegisterRequest,
|
||||
response: Response,
|
||||
db: Session=Depends(get_db)
|
||||
):
|
||||
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)
|
||||
|
||||
try:
|
||||
result = await auth_service.register(db=db, name=request.name, email=request.email, password=request.password, phone=request.phone)
|
||||
response.set_cookie(key='refreshToken', value=result['refreshToken'], httponly=True, secure=False, samesite='strict', max_age=7 * 24 * 60 * 60, path='/')
|
||||
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
|
||||
# Use secure cookies in production (HTTPS required)
|
||||
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,
|
||||
path='/'
|
||||
)
|
||||
|
||||
# Log successful registration
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='user_registered',
|
||||
resource_type='user',
|
||||
user_id=result['user']['id'],
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details={'email': register_request.email, 'name': register_request.name},
|
||||
status='success'
|
||||
)
|
||||
|
||||
return {'status': 'success', 'message': 'Registration successful', 'data': {'token': result['token'], 'user': result['user']}}
|
||||
except ValueError as e:
|
||||
error_message = str(e)
|
||||
# Log failed registration attempt
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='user_registration_failed',
|
||||
resource_type='user',
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details={'email': register_request.email, 'name': register_request.name},
|
||||
status='failed',
|
||||
error_message=error_message
|
||||
)
|
||||
return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={'status': 'error', 'message': error_message})
|
||||
|
||||
@router.post('/login')
|
||||
async def login(request: LoginRequest, response: Response, db: Session=Depends(get_db)):
|
||||
async def login(
|
||||
request: Request,
|
||||
login_request: LoginRequest,
|
||||
response: Response,
|
||||
db: Session=Depends(get_db)
|
||||
):
|
||||
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)
|
||||
|
||||
try:
|
||||
result = await auth_service.login(db=db, email=request.email, password=request.password, remember_me=request.rememberMe or False, mfa_token=request.mfaToken)
|
||||
result = await auth_service.login(db=db, email=login_request.email, password=login_request.password, remember_me=login_request.rememberMe or False, mfa_token=login_request.mfaToken)
|
||||
if result.get('requires_mfa'):
|
||||
# Log MFA required
|
||||
user = db.query(User).filter(User.email == login_request.email.lower().strip()).first()
|
||||
if user:
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='login_mfa_required',
|
||||
resource_type='authentication',
|
||||
user_id=user.id,
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details={'email': login_request.email},
|
||||
status='success'
|
||||
)
|
||||
return {'status': 'success', 'requires_mfa': True, 'user_id': result['user_id']}
|
||||
max_age = 7 * 24 * 60 * 60 if request.rememberMe else 1 * 24 * 60 * 60
|
||||
response.set_cookie(key='refreshToken', value=result['refreshToken'], httponly=True, secure=False, samesite='strict', max_age=max_age, path='/')
|
||||
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)
|
||||
response.set_cookie(
|
||||
key='refreshToken',
|
||||
value=result['refreshToken'],
|
||||
httponly=True,
|
||||
secure=settings.is_production, # Secure flag enabled in production
|
||||
samesite='strict',
|
||||
max_age=max_age,
|
||||
path='/'
|
||||
)
|
||||
|
||||
# Log successful login
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='login_success',
|
||||
resource_type='authentication',
|
||||
user_id=result['user']['id'],
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details={'email': login_request.email, 'remember_me': login_request.rememberMe},
|
||||
status='success'
|
||||
)
|
||||
|
||||
return {'status': 'success', 'data': {'token': result['token'], '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
|
||||
|
||||
# Log failed login attempt
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='login_failed',
|
||||
resource_type='authentication',
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details={'email': login_request.email},
|
||||
status='failed',
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
return JSONResponse(status_code=status_code, content={'status': 'error', 'message': error_message})
|
||||
|
||||
@router.post('/refresh-token', response_model=TokenResponse)
|
||||
@@ -59,10 +186,34 @@ async def refresh_token(refreshToken: str=Cookie(None), db: Session=Depends(get_
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
|
||||
|
||||
@router.post('/logout', response_model=MessageResponse)
|
||||
async def logout(response: Response, refreshToken: str=Cookie(None), db: Session=Depends(get_db)):
|
||||
async def logout(
|
||||
request: Request,
|
||||
response: Response,
|
||||
refreshToken: str=Cookie(None),
|
||||
current_user: User=Depends(get_current_user),
|
||||
db: Session=Depends(get_db)
|
||||
):
|
||||
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)
|
||||
|
||||
if refreshToken:
|
||||
await auth_service.logout(db, refreshToken)
|
||||
response.delete_cookie(key='refreshToken', path='/')
|
||||
|
||||
# Log logout
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='logout',
|
||||
resource_type='authentication',
|
||||
user_id=current_user.id,
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details={'email': current_user.email},
|
||||
status='success'
|
||||
)
|
||||
|
||||
return {'status': 'success', 'message': 'Logout successful'}
|
||||
|
||||
@router.get('/profile')
|
||||
@@ -164,11 +315,12 @@ async def regenerate_backup_codes(current_user: User=Depends(get_current_user),
|
||||
@router.post('/avatar/upload')
|
||||
async def upload_avatar(request: Request, image: UploadFile=File(...), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
if not image.content_type or not image.content_type.startswith('image/'):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='File must be an image')
|
||||
content = await image.read()
|
||||
if len(content) > 2 * 1024 * 1024:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Avatar file size must be less than 2MB')
|
||||
# Use comprehensive file validation (magic bytes + size)
|
||||
from ..utils.file_validation import validate_uploaded_image
|
||||
max_avatar_size = 2 * 1024 * 1024 # 2MB for avatars
|
||||
|
||||
# Validate file completely (MIME type, size, magic bytes, integrity)
|
||||
content = await validate_uploaded_image(image, max_avatar_size)
|
||||
upload_dir = Path(__file__).parent.parent.parent / 'uploads' / 'avatars'
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
if current_user.avatar:
|
||||
|
||||
Reference in New Issue
Block a user