Files
leopost-full/backend/app/routers/affiliates.py
Michele 77ca70cd48 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>
2026-03-31 20:01:07 +02:00

112 lines
3.4 KiB
Python

"""Affiliate links CRUD router.
Manages affiliate links that can be injected into generated content.
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from ..auth import get_current_user
from ..database import get_db
from ..models import AffiliateLink, User
from ..plan_limits import get_plan
from ..schemas import AffiliateLinkCreate, AffiliateLinkResponse, AffiliateLinkUpdate
router = APIRouter(
prefix="/api/affiliates",
tags=["affiliates"],
)
@router.get("/", response_model=list[AffiliateLinkResponse])
def list_affiliate_links(
character_id: int | None = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""List all affiliate links, optionally filtered by character."""
query = db.query(AffiliateLink).filter(AffiliateLink.user_id == current_user.id)
if character_id is not None:
query = query.filter(AffiliateLink.character_id == character_id)
return query.order_by(AffiliateLink.created_at.desc()).all()
@router.get("/{link_id}", response_model=AffiliateLinkResponse)
def get_affiliate_link(
link_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get a single affiliate link by ID."""
link = (
db.query(AffiliateLink)
.filter(AffiliateLink.id == link_id, AffiliateLink.user_id == current_user.id)
.first()
)
if not link:
raise HTTPException(status_code=404, detail="Affiliate link not found")
return link
@router.post("/", response_model=AffiliateLinkResponse, status_code=201)
def create_affiliate_link(
data: AffiliateLinkCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Create a new affiliate link."""
plan = get_plan(current_user)
if not plan.get("affiliate_links"):
raise HTTPException(
status_code=403,
detail={"message": "Affiliate links disponibili solo con Pro.", "upgrade_required": True},
)
link = AffiliateLink(**data.model_dump())
link.user_id = current_user.id
db.add(link)
db.commit()
db.refresh(link)
return link
@router.put("/{link_id}", response_model=AffiliateLinkResponse)
def update_affiliate_link(
link_id: int,
data: AffiliateLinkUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Update an affiliate link."""
link = (
db.query(AffiliateLink)
.filter(AffiliateLink.id == link_id, AffiliateLink.user_id == current_user.id)
.first()
)
if not link:
raise HTTPException(status_code=404, detail="Affiliate link not found")
update_data = data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(link, key, value)
db.commit()
db.refresh(link)
return link
@router.delete("/{link_id}", status_code=204)
def delete_affiliate_link(
link_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Delete an affiliate link."""
link = (
db.query(AffiliateLink)
.filter(AffiliateLink.id == link_id, AffiliateLink.user_id == current_user.id)
.first()
)
if not link:
raise HTTPException(status_code=404, detail="Affiliate link not found")
db.delete(link)
db.commit()