import json import sqlite3 from fastapi import FastAPI, HTTPException, status, Depends from fastapi.middleware.cors import CORSMiddleware from database import init_db, get_db from models import RegisterRequest, LoginRequest, AuthResponse, SaveGameRequest, SyncTickRequest from auth import hash_password, verify_password, create_access_token, get_current_user_id import engine app = FastAPI(title="El Puro API", version="1.1.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.on_event("startup") def startup_event(): init_db() @app.get("/api/health") def health_check(): return {"status": "ok", "app": "El Puro", "version": "1.1.0"} @app.post("/api/auth/register", response_model=AuthResponse) def register(req: RegisterRequest): if req.website_hp and req.website_hp.strip(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Registration verification failed." ) clean_username = req.username.strip() if not clean_username: raise HTTPException(status_code=400, detail="Username cannot be blank.") hashed = hash_password(req.password) try: with get_db() as conn: cursor = conn.cursor() cursor.execute( "INSERT INTO users (username, password_hash) VALUES (?, ?)", (clean_username, hashed) ) user_id = cursor.lastrowid # Initialize clean server authoritative game state init_state = engine.get_initial_state() cursor.execute( "INSERT INTO saves (user_id, save_data, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", (user_id, json.dumps(init_state)) ) conn.commit() except sqlite3.IntegrityError: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Username already taken." ) token = create_access_token({"sub": str(user_id), "username": clean_username}) return AuthResponse(token=token, username=clean_username) @app.post("/api/auth/login", response_model=AuthResponse) def login(req: LoginRequest): clean_username = req.username.strip() with get_db() as conn: cursor = conn.cursor() cursor.execute( "SELECT id, username, password_hash FROM users WHERE username = ?", (clean_username,) ) row = cursor.fetchone() if not row or not verify_password(req.password, row["password_hash"]): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password." ) user_id = row["id"] token = create_access_token({"sub": str(user_id), "username": clean_username}) return AuthResponse(token=token, username=clean_username) @app.get("/api/game/state") def get_game_state(user_id: int = Depends(get_current_user_id)): with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT save_data, updated_at FROM saves WHERE user_id = ?", (user_id,)) row = cursor.fetchone() if not row: init_state = engine.get_initial_state() return {"has_save": True, "save_data": init_state, "updated_at": None} try: data = json.loads(row["save_data"]) # Process authoritative offline progress up to now data = engine.process_offline_and_ticks(data) return {"has_save": True, "save_data": data, "updated_at": row["updated_at"]} except Exception: return {"has_save": True, "save_data": engine.get_initial_state(), "updated_at": None} @app.post("/api/game/tick") def sync_tick(req: SyncTickRequest, user_id: int = Depends(get_current_user_id)): """Authoritative periodic tick (called every 5-10s): applies queued actions and computes tick progress.""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT save_data FROM saves WHERE user_id = ?", (user_id,)) row = cursor.fetchone() if row: try: state = json.loads(row["save_data"]) except Exception: state = engine.get_initial_state() else: state = engine.get_initial_state() # 1. Apply authorized player actions if req.actions: state = engine.apply_actions(state, req.actions) # 2. Advance production, automation, and offline ticks state = engine.process_offline_and_ticks(state) # 3. Persist authoritative clean state cursor.execute(""" INSERT INTO saves (user_id, save_data, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(user_id) DO UPDATE SET save_data = excluded.save_data, updated_at = CURRENT_TIMESTAMP """, (user_id, json.dumps(state))) conn.commit() return {"status": "ok", "save_data": state} @app.post("/api/game/save") def save_game_state(req: SaveGameRequest, user_id: int = Depends(get_current_user_id)): # Sanitize data before persisting clean_state = engine.sanitize_state(req.save_data) json_data = json.dumps(clean_state) with get_db() as conn: cursor = conn.cursor() cursor.execute(""" INSERT INTO saves (user_id, save_data, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(user_id) DO UPDATE SET save_data = excluded.save_data, updated_at = CURRENT_TIMESTAMP """, (user_id, json_data)) conn.commit() return {"status": "saved", "save_data": clean_state}