This commit is contained in:
HotSwapp
2025-08-18 20:20:04 -05:00
parent 89b2bc0aa2
commit bac8cc4bd5
114 changed files with 30258 additions and 1341 deletions

View File

@@ -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"}