Files
leopost-full/backend/app/schemas.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

321 lines
7.5 KiB
Python

from datetime import datetime
from typing import Optional
from pydantic import BaseModel
# === Auth ===
class LoginRequest(BaseModel):
username: str
password: str
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
user: Optional[dict] = None
# === Characters ===
class CharacterBase(BaseModel):
name: str
niche: str
topics: list[str] = []
tone: Optional[str] = None
visual_style: dict = {}
social_accounts: dict = {}
affiliate_links: list[dict] = []
avatar_url: Optional[str] = None
is_active: bool = True
class CharacterCreate(CharacterBase):
pass
class CharacterUpdate(BaseModel):
name: Optional[str] = None
niche: Optional[str] = None
topics: Optional[list[str]] = None
tone: Optional[str] = None
visual_style: Optional[dict] = None
social_accounts: Optional[dict] = None
affiliate_links: Optional[list[dict]] = None
avatar_url: Optional[str] = None
is_active: Optional[bool] = None
class CharacterResponse(CharacterBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# === Posts / Content ===
class PostCreate(BaseModel):
character_id: int
content_type: str = "text"
platform_hint: str = "instagram"
text_content: Optional[str] = None
hashtags: list[str] = []
image_url: Optional[str] = None
video_url: Optional[str] = None
media_urls: list[str] = []
affiliate_links_used: list[dict] = []
status: str = "draft"
class PostUpdate(BaseModel):
text_content: Optional[str] = None
hashtags: Optional[list[str]] = None
image_url: Optional[str] = None
video_url: Optional[str] = None
status: Optional[str] = None
affiliate_links_used: Optional[list[dict]] = None
class PostResponse(BaseModel):
id: int
character_id: int
content_type: str
text_content: Optional[str] = None
hashtags: list[str] = []
image_url: Optional[str] = None
video_url: Optional[str] = None
media_urls: list[str] = []
affiliate_links_used: list[dict] = []
llm_provider: Optional[str] = None
llm_model: Optional[str] = None
platform_hint: Optional[str] = None
status: str
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class GenerateContentRequest(BaseModel):
character_id: int
platform: str = "instagram"
content_type: str = "text"
topic_hint: Optional[str] = None
include_affiliates: bool = True
provider: Optional[str] = None # override default LLM
model: Optional[str] = None
class GenerateImageRequest(BaseModel):
character_id: int
prompt: Optional[str] = None # auto-generated if not provided
style_hint: Optional[str] = None
size: str = "1024x1024"
provider: Optional[str] = None # dalle, replicate
# === Affiliate Links ===
class AffiliateLinkBase(BaseModel):
character_id: Optional[int] = None
network: str
name: str
url: str
tag: Optional[str] = None
topics: list[str] = []
is_active: bool = True
class AffiliateLinkCreate(AffiliateLinkBase):
pass
class AffiliateLinkUpdate(BaseModel):
network: Optional[str] = None
name: Optional[str] = None
url: Optional[str] = None
tag: Optional[str] = None
topics: Optional[list[str]] = None
is_active: Optional[bool] = None
class AffiliateLinkResponse(AffiliateLinkBase):
id: int
click_count: int = 0
created_at: datetime
class Config:
from_attributes = True
# === Editorial Plans ===
class EditorialPlanBase(BaseModel):
character_id: int
name: str
frequency: str = "daily"
posts_per_day: int = 1
platforms: list[str] = []
content_types: list[str] = ["text"]
posting_times: list[str] = ["09:00"]
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
is_active: bool = False
class EditorialPlanCreate(EditorialPlanBase):
pass
class EditorialPlanUpdate(BaseModel):
name: Optional[str] = None
frequency: Optional[str] = None
posts_per_day: Optional[int] = None
platforms: Optional[list[str]] = None
content_types: Optional[list[str]] = None
posting_times: Optional[list[str]] = None
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
is_active: Optional[bool] = None
class EditorialPlanResponse(EditorialPlanBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# === Scheduled Posts ===
class ScheduledPostCreate(BaseModel):
plan_id: Optional[int] = None
post_id: int
platform: str
scheduled_at: datetime
class ScheduledPostResponse(BaseModel):
id: int
plan_id: Optional[int] = None
post_id: int
platform: str
scheduled_at: datetime
published_at: Optional[datetime] = None
status: str
error_message: Optional[str] = None
external_post_id: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
# === Social Accounts ===
class SocialAccountCreate(BaseModel):
character_id: int
platform: str
account_name: Optional[str] = None
account_id: Optional[str] = None
access_token: Optional[str] = None
refresh_token: Optional[str] = None
page_id: Optional[str] = None
extra_data: dict = {}
class SocialAccountUpdate(BaseModel):
account_name: Optional[str] = None
access_token: Optional[str] = None
refresh_token: Optional[str] = None
page_id: Optional[str] = None
extra_data: Optional[dict] = None
is_active: Optional[bool] = None
class SocialAccountResponse(BaseModel):
id: int
character_id: int
platform: str
account_name: Optional[str] = None
account_id: Optional[str] = None
page_id: Optional[str] = None
is_active: bool
token_expires_at: Optional[datetime] = None
created_at: datetime
class Config:
from_attributes = True
# === Comments ===
class CommentResponse(BaseModel):
id: int
scheduled_post_id: Optional[int] = None
platform: str
external_comment_id: Optional[str] = None
author_name: Optional[str] = None
content: Optional[str] = None
ai_suggested_reply: Optional[str] = None
approved_reply: Optional[str] = None
reply_status: str
replied_at: Optional[datetime] = None
created_at: datetime
class Config:
from_attributes = True
class CommentAction(BaseModel):
action: str # approve, edit, ignore
reply_text: Optional[str] = None # for edit action
# === System Settings ===
class SettingUpdate(BaseModel):
value: dict | str | list | int | bool | None
class SettingResponse(BaseModel):
key: str
value: dict | str | list | int | bool | None
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# === Editorial Calendar (from PostGenerator) ===
class CalendarGenerateRequest(BaseModel):
topics: list[str]
format_narrativo: Optional[str] = None # PAS, AIDA, BAB, Storytelling, Listicle, Dato_Implicazione
awareness_level: Optional[int] = None # 1-5 (Schwartz levels)
num_posts: int = 7
start_date: Optional[str] = None # YYYY-MM-DD
character_id: Optional[int] = None
class CalendarSlotResponse(BaseModel):
indice: int
topic: str
formato_narrativo: str
awareness_level: int
awareness_label: str
data_pubblicazione: str
note: Optional[str] = None
class CalendarResponse(BaseModel):
slots: list[CalendarSlotResponse]
totale_post: int