Add full SkinsPins application
- FastAPI + SQLite + Alpine.js + HTMX PWA for Bingo Bango Bongo golf scoring - Passwordless magic link auth with email (aiosmtplib) and admin invite system - Real-time score entry via WebSocket with drag-and-drop player ordering - Course management with per-hole par and stroke index - Scorecard with golf score markers (birdie, eagle, bogey, etc.) - Two-step new game form with tee selection per player - Dashboard with stats, live games social stream, and recent game history - Game summary view (read-only scorecard with leaderboard) - User profile pages with win stats - PWA install support with generated PNG icons - Admin panel for users, courses, games, and invitations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+69
@@ -0,0 +1,69 @@
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from app.database import init_db
|
||||
from app.dependencies import NeedsLogin, NeedsOnboarding, NeedsAdmin
|
||||
from app.routers import auth, onboarding, dashboard, admin, profile, games, courses, users
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
AVATARS_DIR = Path(os.getenv("DATABASE_PATH", "/data/skinspins.db")).parent / "avatars"
|
||||
AVATARS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await init_db()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
app.add_middleware(SessionMiddleware, secret_key=os.getenv("SECRET_KEY", "dev-secret"))
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
app.mount("/avatars", StaticFiles(directory=str(AVATARS_DIR)), name="avatars")
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(onboarding.router)
|
||||
app.include_router(dashboard.router)
|
||||
app.include_router(admin.router)
|
||||
app.include_router(profile.router)
|
||||
app.include_router(games.router)
|
||||
app.include_router(courses.router)
|
||||
app.include_router(users.router)
|
||||
|
||||
|
||||
@app.exception_handler(NeedsLogin)
|
||||
async def needs_login_handler(request: Request, exc: NeedsLogin):
|
||||
response = RedirectResponse("/login", status_code=302)
|
||||
response.delete_cookie("session")
|
||||
return response
|
||||
|
||||
|
||||
@app.exception_handler(NeedsOnboarding)
|
||||
async def needs_onboarding_handler(request: Request, exc: NeedsOnboarding):
|
||||
return RedirectResponse("/onboarding", status_code=302)
|
||||
|
||||
|
||||
@app.exception_handler(NeedsAdmin)
|
||||
async def needs_admin_handler(request: Request, exc: NeedsAdmin):
|
||||
return RedirectResponse("/", status_code=302)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def security_headers(request: Request, call_next):
|
||||
response: Response = await call_next(request)
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
response.headers["X-Frame-Options"] = "DENY"
|
||||
return response
|
||||
Reference in New Issue
Block a user