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

@@ -1,7 +1,9 @@
"""Core authentication utilities used throughout the application."""
from datetime import datetime, timedelta
import bcrypt
from fastapi import APIRouter, Depends, HTTPException
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session
@@ -9,9 +11,7 @@ from sqlalchemy.orm import Session
from .config import settings
from .database import get_db
from .models import User
from .schemas import LoginRequest, Token
router = APIRouter(prefix="/api/auth", tags=["auth"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
@@ -30,31 +30,29 @@ def create_access_token(data: dict) -> str:
return jwt.encode(to_encode, settings.secret_key, algorithm="HS256")
def get_user_by_email(db: Session, email: str) -> User | None:
return db.query(User).filter(User.email == email).first()
def get_current_user(
token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)
) -> User:
try:
payload = jwt.decode(token, settings.secret_key, algorithms=["HS256"])
username: str = payload.get("sub")
if username is None:
# Support both sub (user_id) and legacy username
user_id = payload.get("user_id")
username = payload.get("sub")
if user_id is None and username is None:
raise HTTPException(status_code=401, detail="Invalid token")
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
user = db.query(User).filter(User.username == username).first()
if user_id is not None:
user = db.query(User).filter(User.id == user_id).first()
else:
# Legacy: username-based token
user = db.query(User).filter(User.username == username).first()
if user is None:
raise HTTPException(status_code=401, detail="User not found")
return user
@router.post("/login", response_model=Token)
def login(request: LoginRequest, db: Session = Depends(get_db)):
user = db.query(User).filter(User.username == request.username).first()
if not user or not verify_password(request.password, user.hashed_password):
raise HTTPException(status_code=401, detail="Invalid credentials")
token = create_access_token({"sub": user.username})
return Token(access_token=token)
@router.get("/me")
def me(user: User = Depends(get_current_user)):
return {"username": user.username}