changes
This commit is contained in:
155
app/api/auth.py
155
app/api/auth.py
@@ -20,6 +20,12 @@ from app.auth.security import (
|
||||
get_current_user,
|
||||
get_admin_user,
|
||||
)
|
||||
from app.utils.enhanced_auth import (
|
||||
validate_and_authenticate_user,
|
||||
PasswordValidator,
|
||||
AccountLockoutManager,
|
||||
)
|
||||
from app.utils.session_manager import SessionManager, get_session_manager
|
||||
from app.auth.schemas import (
|
||||
Token,
|
||||
UserCreate,
|
||||
@@ -36,8 +42,13 @@ logger = get_logger("auth")
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(login_data: LoginRequest, request: Request, db: Session = Depends(get_db)):
|
||||
"""Login endpoint"""
|
||||
async def login(
|
||||
login_data: LoginRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
session_manager: SessionManager = Depends(get_session_manager)
|
||||
):
|
||||
"""Enhanced login endpoint with session management and security features"""
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
user_agent = request.headers.get("user-agent", "")
|
||||
|
||||
@@ -48,30 +59,38 @@ async def login(login_data: LoginRequest, request: Request, db: Session = Depend
|
||||
user_agent=user_agent
|
||||
)
|
||||
|
||||
user = authenticate_user(db, login_data.username, login_data.password)
|
||||
if not user:
|
||||
log_auth_attempt(
|
||||
username=login_data.username,
|
||||
success=False,
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
error="Invalid credentials"
|
||||
)
|
||||
# Use enhanced authentication with lockout protection
|
||||
user, auth_errors = validate_and_authenticate_user(
|
||||
db, login_data.username, login_data.password, request
|
||||
)
|
||||
|
||||
if not user or auth_errors:
|
||||
error_message = auth_errors[0] if auth_errors else "Incorrect username or password"
|
||||
|
||||
logger.warning(
|
||||
"Login failed - invalid credentials",
|
||||
"Login failed - enhanced auth",
|
||||
username=login_data.username,
|
||||
client_ip=client_ip
|
||||
client_ip=client_ip,
|
||||
errors=auth_errors
|
||||
)
|
||||
|
||||
# Get lockout info for response headers
|
||||
lockout_info = AccountLockoutManager.get_lockout_info(db, login_data.username)
|
||||
|
||||
headers = {"WWW-Authenticate": "Bearer"}
|
||||
if lockout_info["is_locked"]:
|
||||
headers["X-Account-Locked"] = "true"
|
||||
headers["X-Unlock-Time"] = lockout_info["unlock_time"] or ""
|
||||
else:
|
||||
headers["X-Attempts-Remaining"] = str(lockout_info["attempts_remaining"])
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
detail=error_message,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# Update last login
|
||||
user.last_login = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
|
||||
# Successful authentication - create tokens
|
||||
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
@@ -83,14 +102,8 @@ async def login(login_data: LoginRequest, request: Request, db: Session = Depend
|
||||
db=db,
|
||||
)
|
||||
|
||||
log_auth_attempt(
|
||||
username=login_data.username,
|
||||
success=True,
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent
|
||||
)
|
||||
logger.info(
|
||||
"Login successful",
|
||||
"Login successful - enhanced auth",
|
||||
username=login_data.username,
|
||||
user_id=user.id,
|
||||
client_ip=client_ip
|
||||
@@ -105,7 +118,15 @@ async def register(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user) # Only admins can create users
|
||||
):
|
||||
"""Register new user (admin only)"""
|
||||
"""Register new user with password validation (admin only)"""
|
||||
# Validate password strength
|
||||
is_valid, password_errors = PasswordValidator.validate_password_strength(user_data.password)
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Password validation failed: {'; '.join(password_errors)}"
|
||||
)
|
||||
|
||||
# Check if username or email already exists
|
||||
existing_user = db.query(User).filter(
|
||||
(User.username == user_data.username) | (User.email == user_data.email)
|
||||
@@ -130,6 +151,12 @@ async def register(
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
|
||||
logger.info(
|
||||
"User registered",
|
||||
username=new_user.username,
|
||||
created_by=current_user.username
|
||||
)
|
||||
|
||||
return new_user
|
||||
|
||||
|
||||
@@ -257,4 +284,76 @@ async def update_theme_preference(
|
||||
current_user.theme_preference = theme_data.theme_preference
|
||||
db.commit()
|
||||
|
||||
return {"message": "Theme preference updated successfully", "theme": theme_data.theme_preference}
|
||||
return {"message": "Theme preference updated successfully", "theme": theme_data.theme_preference}
|
||||
|
||||
|
||||
@router.post("/validate-password")
|
||||
async def validate_password(password_data: dict):
|
||||
"""Validate password strength and return detailed feedback"""
|
||||
password = password_data.get("password", "")
|
||||
|
||||
if not password:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Password is required"
|
||||
)
|
||||
|
||||
is_valid, errors = PasswordValidator.validate_password_strength(password)
|
||||
strength_score = PasswordValidator.generate_password_strength_score(password)
|
||||
|
||||
return {
|
||||
"is_valid": is_valid,
|
||||
"errors": errors,
|
||||
"strength_score": strength_score,
|
||||
"strength_level": (
|
||||
"Very Weak" if strength_score < 20 else
|
||||
"Weak" if strength_score < 40 else
|
||||
"Fair" if strength_score < 60 else
|
||||
"Good" if strength_score < 80 else
|
||||
"Strong"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/account-status/{username}")
|
||||
async def get_account_status(
|
||||
username: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user) # Admin only endpoint
|
||||
):
|
||||
"""Get account lockout status and security information (admin only)"""
|
||||
lockout_info = AccountLockoutManager.get_lockout_info(db, username)
|
||||
|
||||
# Get recent login attempts
|
||||
from app.utils.enhanced_auth import SuspiciousActivityDetector
|
||||
is_suspicious, warnings = SuspiciousActivityDetector.is_login_suspicious(
|
||||
db, username, "admin-check", "admin-request"
|
||||
)
|
||||
|
||||
return {
|
||||
"username": username,
|
||||
"lockout_info": lockout_info,
|
||||
"suspicious_activity": {
|
||||
"is_suspicious": is_suspicious,
|
||||
"warnings": warnings
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/unlock-account/{username}")
|
||||
async def unlock_account(
|
||||
username: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user) # Admin only endpoint
|
||||
):
|
||||
"""Manually unlock a user account (admin only)"""
|
||||
# Reset failed attempts by recording a successful "admin unlock"
|
||||
AccountLockoutManager.reset_failed_attempts(db, username)
|
||||
|
||||
logger.info(
|
||||
"Account manually unlocked",
|
||||
username=username,
|
||||
unlocked_by=current_user.username
|
||||
)
|
||||
|
||||
return {"message": f"Account '{username}' has been unlocked"}
|
||||
Reference in New Issue
Block a user