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>
This commit is contained in:
chihlasm
2026-02-13 01:42:51 -05:00
parent b8f25f19eb
commit ad59446332
32 changed files with 3064 additions and 38 deletions

View File

@@ -1,8 +1,8 @@
"""Pydantic schemas for admin panel endpoints."""
from datetime import datetime
from typing import Optional
from typing import Literal, Optional
from uuid import UUID
from pydantic import BaseModel, Field
from pydantic import BaseModel, EmailStr, Field
# --- Dashboard ---
@@ -206,3 +206,39 @@ class GlobalCategoryResponse(BaseModel):
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