Files
echoes-of-the-ash/api/routers/auth.py
2025-11-27 16:27:01 +01:00

385 lines
12 KiB
Python

"""
Authentication router.
Handles user registration, login, and profile retrieval.
"""
from fastapi import APIRouter, HTTPException, Depends, status
from typing import Dict, Any
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
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": [
{
"id": char["id"],
"name": char["name"],
"level": char["level"],
"xp": char["xp"],
"hp": char["hp"],
"max_hp": char["max_hp"],
"stamina": char["stamina"],
"max_stamina": char["max_stamina"],
"strength": char["strength"],
"agility": char["agility"],
"endurance": char["endurance"],
"intellect": char["intellect"],
"avatar_data": char.get("avatar_data"),
"last_played_at": char.get("last_played_at"),
"location_id": char["location_id"],
}
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": [
{
"id": char["id"],
"name": char["name"],
"level": char["level"],
"xp": char["xp"],
"hp": char["hp"],
"max_hp": char["max_hp"],
"location_id": char["location_id"],
"avatar_data": char.get("avatar_data"),
"last_played_at": char.get("last_played_at"),
}
for char in characters
]
}
@router.post("/change-email")
async def change_email(
request: "ChangeEmailRequest",
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Change account email address"""
from ..services.models import ChangeEmailRequest
# 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": "Email updated successfully", "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",
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Change account password"""
from ..services.models import ChangePasswordRequest
# 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": "Password updated successfully"}
@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": [
{
"id": char["id"],
"name": char["name"],
"level": char["level"],
"xp": char["xp"],
"hp": char["hp"],
"max_hp": char["max_hp"],
"location_id": char["location_id"],
"avatar_data": char.get("avatar_data"),
"last_played_at": char.get("last_played_at")
}
for char in characters
],
"needs_character_creation": len(characters) == 0
}