Commit
This commit is contained in:
384
api/routers/auth.py
Normal file
384
api/routers/auth.py
Normal file
@@ -0,0 +1,384 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user