Files
resolutionflow/backend/app/schemas/admin.py
chihlasm ad59446332 feat: user management — admin create, password reset, archive/delete, quick invite
Phase 1: must_change_password enforcement + change password endpoint/page
Phase 2: Admin user creation (M365-style) with temp password
Phase 3: Password reset (self-service forgot + admin-triggered)
Phase 4: User archive (soft delete) + hard delete with precheck
Phase 5: Quick invite from admin Users page

Also fixes:
- Auto-create subscription for accounts missing one
- Hard delete precheck ignores sole-member personal accounts
- Seed script patches tree nodes for validation compliance

Migrations: 031 (must_change_password), 032 (password_reset_tokens), 033 (user soft delete)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:42:51 -05:00

245 lines
5.8 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
# --- 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 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"])
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["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