Files
resolutionflow/backend/app/schemas/admin.py
Michael Chihlas f1be3abcc5
Some checks failed
CI / e2e (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / backend (push) Has been cancelled
Mirror to GitHub / mirror (push) Has been cancelled
feat: self-serve signup Phase 2 (frontend cutover) (#162)
Co-authored-by: Michael Chihlas <michael@resolutionflow.com>
Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
2026-05-07 18:42:20 +00:00

378 lines
9.7 KiB
Python

"""Pydantic schemas for admin panel endpoints."""
from datetime import datetime
from typing import Literal, Optional
from uuid import UUID
from pydantic import BaseModel, EmailStr, Field
# --- Dashboard ---
class DashboardMetrics(BaseModel):
total_users: int
active_subscriptions: int
paid_accounts: int
total_trees: int
class ActivityEntry(BaseModel):
id: UUID
user_email: Optional[str] = None
action: str
resource_type: str
resource_id: Optional[UUID] = None
details: Optional[dict] = None
ip_address: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
# --- Admin Accounts & People Search ---
class AdminUserListItem(BaseModel):
id: UUID
email: EmailStr
name: str
role: str
is_super_admin: bool = False
is_active: bool = True
account_id: Optional[UUID] = None
account_role: Optional[str] = None
account_name: Optional[str] = None
account_display_code: Optional[str] = None
created_at: datetime
last_login: Optional[datetime] = None
deleted_at: Optional[datetime] = None
class AdminUserListResponse(BaseModel):
items: list[AdminUserListItem]
total: int
page: int
per_page: int
class AdminAccountMember(BaseModel):
id: UUID
email: EmailStr
name: str
role: str
is_super_admin: bool = False
is_active: bool = True
account_role: Optional[str] = None
created_at: datetime
last_login: Optional[datetime] = None
deleted_at: Optional[datetime] = None
class AdminAccountOwnerSummary(BaseModel):
id: UUID
name: str
email: EmailStr
class AdminAccountSubscriptionSummary(BaseModel):
id: UUID
plan: str
status: str
billing_interval: Optional[str] = None
current_period_end: Optional[datetime] = None
cancel_at_period_end: bool = False
class AdminAccountUsageSummary(BaseModel):
tree_count: int = 0
session_count_this_month: int = 0
class AdminAccountInviteSummary(BaseModel):
id: UUID
email: EmailStr
role: str
expires_at: Optional[datetime] = None
created_at: datetime
used_at: Optional[datetime] = None
class AdminAccountListItem(BaseModel):
id: UUID
name: str
display_code: str
created_at: datetime
owner_id: Optional[UUID] = None
owner: Optional[AdminAccountOwnerSummary] = None
subscription: Optional[AdminAccountSubscriptionSummary] = None
usage: AdminAccountUsageSummary = Field(default_factory=AdminAccountUsageSummary)
member_count: int = 0
active_member_count: int = 0
pending_invite_count: int = 0
sso_enabled: bool = False
branding_company_name: Optional[str] = None
members: list[AdminAccountMember] = Field(default_factory=list)
class AdminAccountListResponse(BaseModel):
items: list[AdminAccountListItem]
total: int
page: int
per_page: int
class AdminAccountDetailResponse(AdminAccountListItem):
invites: list[AdminAccountInviteSummary] = Field(default_factory=list)
class AdminAccountCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
plan: Literal["free", "pro", "team"] = "free"
owner_email: Optional[EmailStr] = Field(None, description="Email of an existing user to set as owner")
class AdminAccountUpdate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
# --- Audit Logs ---
class AuditLogEntry(BaseModel):
id: UUID
user_id: UUID
user_email: Optional[str] = None
action: str
resource_type: str
resource_id: Optional[UUID] = None
details: Optional[dict] = None
ip_address: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
class AuditLogListResponse(BaseModel):
items: list[AuditLogEntry]
total: int
page: int
per_page: int
# --- Plan Limits ---
class PlanLimitResponse(BaseModel):
plan: str
max_trees: Optional[int] = None
max_sessions_per_month: Optional[int] = None
max_users: Optional[int] = None
custom_branding: bool = False
priority_support: bool = False
export_formats: list = []
class Config:
from_attributes = True
class PlanLimitWithBillingResponse(PlanLimitResponse):
"""PlanLimits + plan_billing fields merged. Billing fields are None when no
plan_billing row exists for the plan yet."""
display_name: Optional[str] = None
description: Optional[str] = None
monthly_price_cents: Optional[int] = None
annual_price_cents: Optional[int] = None
stripe_product_id: Optional[str] = None
stripe_monthly_price_id: Optional[str] = None
stripe_annual_price_id: Optional[str] = None
is_public: Optional[bool] = None
is_archived: Optional[bool] = None
sort_order: Optional[int] = None
class PlanLimitUpdate(BaseModel):
plan: str
max_trees: Optional[int] = None
max_sessions_per_month: Optional[int] = None
max_users: Optional[int] = None
custom_branding: bool = False
priority_support: bool = False
export_formats: list = Field(default_factory=lambda: ["markdown", "text"])
# plan_billing fields — all optional, partial-update semantics. If any are
# set in the body, the admin endpoint upserts the plan_billing row in the
# same transaction.
display_name: Optional[str] = None
description: Optional[str] = None
monthly_price_cents: Optional[int] = None
annual_price_cents: Optional[int] = None
stripe_product_id: Optional[str] = None
stripe_monthly_price_id: Optional[str] = None
stripe_annual_price_id: Optional[str] = None
is_public: Optional[bool] = None
is_archived: Optional[bool] = None
sort_order: Optional[int] = None
class AccountOverrideCreate(BaseModel):
account_display_code: str = Field(..., description="Account display code to look up")
override_max_trees: Optional[int] = None
override_max_sessions_per_month: Optional[int] = None
override_max_users: Optional[int] = None
note: Optional[str] = None
class AccountOverrideUpdate(BaseModel):
override_max_trees: Optional[int] = None
override_max_sessions_per_month: Optional[int] = None
override_max_users: Optional[int] = None
note: Optional[str] = None
class AccountOverrideResponse(BaseModel):
id: UUID
account_id: UUID
account_name: Optional[str] = None
account_display_code: Optional[str] = None
override_max_trees: Optional[int] = None
override_max_sessions_per_month: Optional[int] = None
override_max_users: Optional[int] = None
note: Optional[str] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# --- Feature Flags ---
class FeatureFlagCreate(BaseModel):
flag_key: str = Field(..., max_length=100)
display_name: str = Field(..., max_length=255)
description: Optional[str] = None
class FeatureFlagUpdate(BaseModel):
display_name: Optional[str] = None
description: Optional[str] = None
class PlanDefaultEntry(BaseModel):
plan: str
enabled: bool
class FeatureFlagResponse(BaseModel):
id: UUID
flag_key: str
display_name: str
description: Optional[str] = None
plan_defaults: list[PlanDefaultEntry] = []
created_at: datetime
class Config:
from_attributes = True
class PlanDefaultUpdate(BaseModel):
plan: str
flag_id: UUID
enabled: bool
class AccountFeatureOverrideCreate(BaseModel):
account_display_code: str
flag_id: UUID
enabled: bool
note: Optional[str] = None
class AccountFeatureOverrideResponse(BaseModel):
id: UUID
account_id: UUID
account_display_code: Optional[str] = None
flag_id: UUID
flag_key: Optional[str] = None
flag_display_name: Optional[str] = None
enabled: bool
note: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
# --- Platform Settings ---
class SettingsResponse(BaseModel):
settings: dict
class SettingsUpdate(BaseModel):
settings: dict = Field(..., description="Key-value pairs to update")
# --- Global Categories ---
class GlobalCategoryCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
slug: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = None
class GlobalCategoryUpdate(BaseModel):
name: Optional[str] = Field(None, max_length=100)
slug: Optional[str] = Field(None, max_length=100)
description: Optional[str] = None
class GlobalCategoryResponse(BaseModel):
id: UUID
name: str
slug: str
description: Optional[str] = None
account_id: Optional[UUID] = None
tree_count: int = 0
class Config:
from_attributes = True
# --- Move User ---
class MoveUserAccount(BaseModel):
display_code: str = Field(..., description="Target account display code")
# --- Admin User Creation ---
class AdminUserCreate(BaseModel):
email: EmailStr
name: str = Field(..., min_length=1, max_length=255)
account_mode: Literal["existing", "personal"]
account_display_code: Optional[str] = Field(None, description="Required when account_mode='existing'")
account_role: Optional[Literal["owner", "admin", "engineer", "viewer"]] = Field(None, description="Required when account_mode='existing'")
send_email: bool = True
class AdminUserCreateResponse(BaseModel):
user: dict # UserResponse fields
temporary_password: str
email_sent: bool
# --- Admin Password Reset ---
class AdminPasswordReset(BaseModel):
mode: Literal["email_link", "temp_password"]
class AdminPasswordResetResponse(BaseModel):
message: str
temporary_password: Optional[str] = None
email_sent: bool = False
# --- Hard Delete Precheck ---
class HardDeleteCheckResponse(BaseModel):
can_delete: bool
blockers: dict