""" Authentication router. Handles user registration, login, and profile retrieval. """ from fastapi import APIRouter, HTTPException, Depends, status, Request from typing import Dict, Any from ..services.helpers import get_game_message from ..core.security import create_access_token, hash_password, verify_password, get_current_user from ..services.models import UserRegister, UserLogin from .. import database as db from ..items import items_manager from ..services.helpers import calculate_player_capacity, enrich_character_data router = APIRouter(prefix="/api/auth", tags=["authentication"]) @router.post("/register") async def register(user: UserRegister): """Register a new account""" # Check if email already exists existing = await db.get_account_by_email(user.email) if existing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered" ) # Hash password password_hash = hash_password(user.password) # Create account account = await db.create_account( email=user.email, password_hash=password_hash, account_type="web" ) if not account: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create account" ) # Get characters for this account (should be empty for new account) characters = await db.get_characters_by_account_id(account["id"]) # Create access token with account_id (no character selected yet) access_token = create_access_token({ "account_id": account["id"], "character_id": None }) return { "access_token": access_token, "token_type": "bearer", "account": { "id": account["id"], "email": account["email"], "account_type": account["account_type"], "is_premium": account.get("premium_expires_at") is not None, }, "characters": characters, "needs_character_creation": len(characters) == 0 } @router.post("/login") async def login(user: UserLogin): """Login with email and password""" # Get account by email account = await db.get_account_by_email(user.email) if not account: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password" ) # Verify password if not account.get('password_hash'): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password" ) if not verify_password(user.password, account['password_hash']): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password" ) # Update last login await db.update_account_last_login(account["id"]) # Get characters for this account characters = await db.get_characters_by_account_id(account["id"]) # Create access token with account_id (no character selected yet) access_token = create_access_token({ "account_id": account["id"], "character_id": None }) return { "access_token": access_token, "token_type": "bearer", "account": { "id": account["id"], "email": account["email"], "account_type": account["account_type"], "is_premium": account.get("premium_expires_at") is not None, }, "characters": [ await enrich_character_data(char, items_manager) for char in characters ], "needs_character_creation": len(characters) == 0 } @router.get("/me") async def get_me(current_user: Dict[str, Any] = Depends(get_current_user)): """Get current user profile""" return { "id": current_user["id"], "username": current_user.get("username"), "name": current_user["name"], "level": current_user["level"], "xp": current_user["xp"], "hp": current_user["hp"], "max_hp": current_user["max_hp"], "stamina": current_user["stamina"], "max_stamina": current_user["max_stamina"], "strength": current_user["strength"], "agility": current_user["agility"], "endurance": current_user["endurance"], "intellect": current_user["intellect"], "location_id": current_user["location_id"], "is_dead": current_user["is_dead"], "unspent_points": current_user["unspent_points"] } @router.get("/account") async def get_account(current_user: Dict[str, Any] = Depends(get_current_user)): """Get current account details including characters""" # Get account from current user's account_id account_id = current_user.get("account_id") if not account_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No account associated with this user" ) account = await db.get_account_by_id(account_id) if not account: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Account not found" ) # Get characters for this account characters = await db.get_characters_by_account_id(account_id) return { "account": { "id": account["id"], "email": account["email"], "account_type": account["account_type"], "is_premium": account.get("premium_expires_at") is not None and account.get("premium_expires_at") > 0, "premium_expires_at": account.get("premium_expires_at"), "created_at": account.get("created_at"), "last_login_at": account.get("last_login_at"), }, "characters": [ await enrich_character_data(char, items_manager) for char in characters ] } @router.post("/change-email") async def change_email( request: "ChangeEmailRequest", req: Request, current_user: Dict[str, Any] = Depends(get_current_user) ): """Change account email address""" from ..services.models import ChangeEmailRequest locale = req.headers.get('Accept-Language', 'en') # Get account account_id = current_user.get("account_id") if not account_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No account associated with this user" ) account = await db.get_account_by_id(account_id) if not account: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Account not found" ) # Verify current password if not account.get('password_hash'): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="This account does not have a password set" ) if not verify_password(request.current_password, account['password_hash']): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Current password is incorrect" ) # Validate new email format import re email_regex = r'^[^\s@]+@[^\s@]+\.[^\s@]+$' if not re.match(email_regex, request.new_email): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid email format" ) # Update email try: await db.update_account_email(account_id, request.new_email) return {"message": get_game_message('email_updated', locale), "new_email": request.new_email} except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) @router.post("/change-password") async def change_password( request: "ChangePasswordRequest", req: Request, current_user: Dict[str, Any] = Depends(get_current_user) ): """Change account password""" from ..services.models import ChangePasswordRequest locale = req.headers.get('Accept-Language', 'en') # Get account account_id = current_user.get("account_id") if not account_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No account associated with this user" ) account = await db.get_account_by_id(account_id) if not account: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Account not found" ) # Verify current password if not account.get('password_hash'): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="This account does not have a password set" ) if not verify_password(request.current_password, account['password_hash']): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Current password is incorrect" ) # Validate new password if len(request.new_password) < 6: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="New password must be at least 6 characters" ) # Hash and update password new_password_hash = hash_password(request.new_password) await db.update_account_password(account_id, new_password_hash) return {"message": get_game_message('password_updated', locale)} @router.post("/steam-login") async def steam_login(steam_data: Dict[str, Any]): """ Login or register with Steam account. Creates account if it doesn't exist. """ steam_id = steam_data.get("steam_id") steam_name = steam_data.get("steam_name", "Steam User") if not steam_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Steam ID is required" ) # Try to find existing account by steam_id account = await db.get_account_by_steam_id(steam_id) if not account: # Create new Steam account # Use steam_id as email (unique identifier) email = f"steam_{steam_id}@steamuser.local" account = await db.create_account( email=email, password_hash=None, # Steam accounts don't have passwords account_type="steam", steam_id=steam_id ) if not account: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create Steam account" ) # Get characters for this account characters = await db.get_characters_by_account_id(account["id"]) # Create access token with account_id (no character selected yet) access_token = create_access_token({ "account_id": account["id"], "character_id": None }) return { "access_token": access_token, "token_type": "bearer", "account": { "id": account["id"], "email": account["email"], "account_type": account["account_type"], "steam_id": steam_id, "steam_name": steam_name, "premium_expires_at": account.get("premium_expires_at"), "created_at": account.get("created_at"), "last_login_at": account.get("last_login_at") }, "characters": [ await enrich_character_data(char, items_manager) for char in characters ], "needs_character_creation": len(characters) == 0 }