feat: multi-user SaaS, piani Freemium/Pro, Google OAuth, admin panel

BLOCCO 1 - Multi-user data model:
- User: email, display_name, avatar_url, auth_provider, google_id
- User: subscription_plan, subscription_expires_at, is_admin, post counters
- SubscriptionCode table per redeem codes
- user_id FK su Character, Post, AffiliateLink, EditorialPlan, SocialAccount, SystemSetting
- Migrazione SQLite-safe (ALTER TABLE) + preserva dati esistenti

BLOCCO 2 - Auth completo:
- Registrazione email/password + login multi-user
- Google OAuth 2.0 (httpx, no deps esterne)
- Callback flow: Google -> /auth/callback?token=JWT -> frontend
- Backward compat login admin con username

BLOCCO 3 - Piani e abbonamenti:
- Freemium: 1 character, 15 post/mese, FB+IG only, no auto-plans
- Pro: illimitato, tutte le piattaforme, tutte le feature
- Enforcement automatico in tutti i router
- Redeem codes con durate 1/3/6/12 mesi
- Admin panel: genera codici, lista utenti

BLOCCO 4 - Frontend completo:
- Login page design Leopost (split coral/cream, Google, social coming soon)
- AuthCallback per OAuth redirect
- PlanBanner, UpgradeModal con pricing
- AdminSettings per generazione codici
- CharacterForm con tab Account Social + guide setup

Deploy:
- Dockerfile con ARG VITE_BASE_PATH/VITE_API_BASE
- docker-compose.prod.yml per leopost.it (no subpath)
- docker-compose.yml aggiornato per lab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Michele
2026-03-31 20:01:07 +02:00
parent 2c16407f96
commit 77ca70cd48
31 changed files with 2818 additions and 449 deletions

View File

@@ -16,10 +16,11 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from .auth import hash_password
from .auth import router as auth_router
from .config import settings
from .database import Base, SessionLocal, engine
from .database import Base, SessionLocal, engine, run_migrations
from .models import User
from .routers.admin import router as admin_router
from .routers.auth import router as auth_router
from .routers.affiliates import router as affiliates_router
from .routers.characters import router as characters_router
from .routers.comments import router as comments_router
@@ -78,10 +79,13 @@ async def lifespan(app: FastAPI):
data_dir = Path("./data")
data_dir.mkdir(parents=True, exist_ok=True)
# Create tables
# Run migrations FIRST (add new columns to existing tables)
run_migrations(engine)
# Create tables (for new tables like subscription_codes)
Base.metadata.create_all(bind=engine)
# Create admin user if not exists
# Create or update admin user
db = SessionLocal()
try:
existing = db.query(User).filter(User.username == settings.admin_username).first()
@@ -89,9 +93,28 @@ async def lifespan(app: FastAPI):
admin = User(
username=settings.admin_username,
hashed_password=hash_password(settings.admin_password),
email="admin@leopost.it",
display_name="Admin",
auth_provider="local",
subscription_plan="pro",
is_admin=True,
)
db.add(admin)
db.commit()
else:
# Update existing admin to ensure proper flags
updated = False
if not existing.is_admin:
existing.is_admin = True
updated = True
if existing.subscription_plan != "pro":
existing.subscription_plan = "pro"
updated = True
if not existing.email:
existing.email = "admin@leopost.it"
updated = True
if updated:
db.commit()
finally:
db.close()
@@ -114,13 +137,17 @@ async def lifespan(app: FastAPI):
# CRITICAL: Do NOT pass root_path here — use Uvicorn --root-path instead.
app = FastAPI(
title="Leopost Full",
version="0.1.0",
version="0.2.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_origins=[
"http://localhost:5173",
"https://leopost.it",
"https://www.leopost.it",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -131,6 +158,7 @@ app.add_middleware(
# ---------------------------------------------------------------------------
app.include_router(auth_router)
app.include_router(admin_router)
app.include_router(characters_router)
app.include_router(content_router)
app.include_router(affiliates_router)
@@ -143,7 +171,7 @@ app.include_router(editorial_router)
@app.get("/api/health")
def health():
return {"status": "ok", "version": "0.1.0"}
return {"status": "ok", "version": "0.2.0"}
# ---------------------------------------------------------------------------