SurfSense/_bmad-output/implementation-artifacts/5-5-admin-seed-and-approval-flow.md
Vonic 4eb6ed18d6 Epic 5 Complete: Billing, Subscriptions, and Admin Features
Resolve all 5 deferred items from Epic 5 adversarial code review:
- Migration 124: Add CASCADE to subscriptionstatus enum drop (prevent orphaned references)
- Stripe rate limiting: In-memory per-user limiter (20 calls/60s) on verify-checkout-session
- Subscription request cooldown: 24h cooldown before resubmitting rejected requests
- Token reset date: Initialize on first subscription activation
- Checkout URL validation: Confirmed HTTPS-only (Stripe always returns HTTPS)

Implement Story 5.4 (Usage Tracking & Rate Limit Enforcement):
- Page quota pre-check at HTTP upload layer
- Extend UserRead schema with token quota fields
- Frontend 402 error handling in document upload
- Quota indicator in dashboard sidebar

Story 5.5 (Admin Seed & Approval Flow):
- Seed admin user migration with default credentials warning
- Subscription approval/rejection routes with admin guard
- 24h rejection cooldown enforcement

Story 5.6 (Admin-Only Model Config):
- Global model config visible across all search spaces
- Per-search-space model configs with user access control
- Superuser CRUD for global configs

Additional fixes from code review:
- PageLimitService: PAST_DUE subscriptions enforce free-tier limits
- TokenQuotaService: PAST_DUE subscriptions enforce free-tier limits
- Config routes: Fixed user_id.is_(None) filter on mutation endpoints
- Stripe webhook: Added guard against silent plan downgrade on unrecognized price_id

All changes formatted with Ruff (Python) and Biome (TypeScript).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 03:54:45 +07:00

10 KiB

Story 5.5: Admin Seed Account & Admin-Approval Subscription Flow

Status: done

Story

As a Kỹ sư / Admin, I want hệ thống tự tạo sẵn một tài khoản admin với đầy đủ quyền khi khởi động lần đầu, và khi Stripe chưa được cấu hình thì vẫn có thể test toàn bộ luồng Pro subscription thông qua giao diện duyệt thủ công của admin, so that development và testing không bị chặn bởi việc thiếu Stripe credentials.

Acceptance Criteria

  1. Khi chạy alembic upgrade head trên database trống, hệ thống tự seed một user admin với thông tin mặc định (admin@surfsense.local / Admin@SurfSense1), overridable qua env vars ADMIN_EMAIL / ADMIN_PASSWORD.
  2. Admin được seed với is_superuser=TRUE, subscription_status='active', plan_id='pro_yearly', monthly_token_limit=1_000_000, pages_limit=5000 và có đủ search space, roles, membership, và default prompts.
  3. Migration seed là idempotent: nếu đã có bất kỳ user nào trong DB thì bỏ qua, không insert lại.
  4. Superuser có thể xem danh sách pending subscription requests tại GET /api/v1/admin/subscription-requests.
  5. Superuser có thể approve request: POST /api/v1/admin/subscription-requests/{id}/approve → user được activate Pro plan ngay lập tức (không cần Stripe).
  6. Superuser có thể reject request: POST /api/v1/admin/subscription-requests/{id}/reject → request bị đánh dấu rejected.
  7. Non-superuser bị từ chối với HTTP 403 khi truy cập các endpoint admin.
  8. Frontend tại /admin/subscription-requests hiển thị bảng pending requests với nút Approve / Reject; chuyển hướng về /login nếu chưa đăng nhập, hiển thị "Access denied" nếu không có quyền superuser.

As-Is (Code trước Story này)

Component Hiện trạng
Admin user Không có — DB trống sau fresh install
Subscription flow khi không có Stripe Trả về HTTP 503 (xem Story 5.2)
Admin routes Không có
subscription_requests table Không có
SubscriptionRequest model Không có

Tasks / Subtasks

  • Task 1: Admin Seed Migration

    • Subtask 1.1: Tạo migration 126_seed_admin_user.py — chỉ insert khi SELECT 1 FROM "user" LIMIT 1 trả về empty.
    • Subtask 1.2: Hash password bằng argon2-cffi (đã cài sẵn qua fastapi-users) bên trong migration function.
    • Subtask 1.3: Insert admin user với tất cả subscription fields đầy đủ.
    • Subtask 1.4: Insert default search space (với citations_enabled=TRUE), Owner/Editor/Viewer roles, owner membership.
    • Subtask 1.5: Insert 8 default prompts (fix-grammar, make-shorter, translate, rewrite, summarize, explain, ask-knowledge-base, look-up-web) với ON CONFLICT DO NOTHING.
    • Subtask 1.6: Downgrade là no-op (không xóa users).
  • Task 2: Subscription Requests Table Migration

    • Subtask 2.1: Tạo migration 127_add_subscription_requests_table.py — dùng raw SQL để tránh SQLAlchemy enum auto-create conflict.
    • Subtask 2.2: DROP TYPE IF EXISTS subscriptionrequeststatus trước khi CREATE TYPE ... AS ENUM ('pending', 'approved', 'rejected').
    • Subtask 2.3: Tạo bảng subscription_requests với columns: id (UUID PK), user_id (FK → user CASCADE), plan_id (VARCHAR 50), status (subscriptionrequeststatus DEFAULT 'pending'), created_at, approved_at (nullable), approved_by (FK → user nullable).
    • Subtask 2.4: Tạo index trên user_id.
  • Task 3: SubscriptionRequest Model & ORM

    • Subtask 3.1: Thêm SubscriptionRequestStatus(StrEnum) enum vào db.py — values: PENDING="pending", APPROVED="approved", REJECTED="rejected".
    • Subtask 3.2: Thêm SubscriptionRequest(Base) model sau class PagePurchase trong db.py.
    • Subtask 3.3: Thêm values_callable=lambda x: [e.value for e in x] vào tất cả SQLAlchemyEnum(SubscriptionStatus)SQLAlchemyEnum(SubscriptionRequestStatus) — bắt buộc để ORM map DB lowercase values thay vì enum member names uppercase.
    • Subtask 3.4: Thêm relationship subscription_requests vào cả hai nhánh User model (LOCAL và Google OAuth).
  • Task 4: Admin Routes Backend

    • Subtask 4.1: Tạo surfsense_backend/app/routes/admin_routes.py với APIRouter(prefix="/admin").
    • Subtask 4.2: Dùng fastapi_users.current_user(active=True, superuser=True) làm dependency — tự động trả 403 cho non-superuser.
    • Subtask 4.3: GET /admin/subscription-requests — query tất cả PENDING requests, JOIN lấy user email, trả về List[SubscriptionRequestItem].
    • Subtask 4.4: POST /admin/subscription-requests/{id}/approve — set status=APPROVED, approved_at=now(), approved_by=current_user.id; activate user subscription dùng cùng logic với _activate_subscription_from_checkout (Story 5.3): set subscription_status=ACTIVE, plan_id, monthly_token_limit, pages_limit=max(pages_used, plan_limit), tokens_used_this_month=0, token_reset_date=today.
    • Subtask 4.5: POST /admin/subscription-requests/{id}/reject — set status=REJECTED.
    • Subtask 4.6: Đăng ký router trong surfsense_backend/app/routes/__init__.pyapp.py.
  • Task 5: Admin Frontend Page

    • Subtask 5.1: Tạo surfsense_web/app/admin/subscription-requests/page.tsx — client component.
    • Subtask 5.2: Auth guard: gọi isAuthenticated() — nếu false redirect /login; nếu API trả 403 hiển thị "Access denied. Superuser privileges required."
    • Subtask 5.3: Hiển thị bảng: User email | Plan | Requested At | Actions (Approve / Reject).
    • Subtask 5.4: Approve/Reject gọi endpoint tương ứng; sau khi thành công xóa row khỏi danh sách local.

Dev Notes

Tại sao cần Admin Seed?

Fresh install không có user nào → không thể login để test. Admin seed giải quyết cold-start problem, đặc biệt cho CI/CD và development.

Tại sao dùng raw SQL trong migration 127?

op.create_table với SQLAlchemy.Enum(create_type=True/False) vẫn trigger _on_table_create event tự động tạo enum type. Dùng raw SQL tránh DuplicateObjectError khi enum đã tồn tại từ Base.metadata.create_all().

ORM values_callable là bắt buộc

SQLAlchemy Enum mặc định dùng member names (uppercase: FREE, ACTIVE) để map vào DB, nhưng migration tạo enum với values lowercase (free, active). Không có values_callableLookupError: 'active' is not among enum values; Possible values: FREE, ACTIVE. Fix: values_callable=lambda x: [e.value for e in x].

Admin Approval Activation Logic

Reuse PLAN_LIMITS config từ config/__init__.py. pages_limit = max(user.pages_used, PLAN_LIMITS[plan_id]["pages_limit"]) để không lock out user khỏi content đã upload.

Luồng test E2E (không có Stripe)

  1. Register user → Login → /pricing → "Upgrade to Pro" → toast "Subscription request submitted"
  2. Login admin (admin@surfsense.local / Admin@SurfSense1) → /admin/subscription-requests → Approve
  3. Login lại user → DB: subscription_status=active, plan_id=pro_monthly, monthly_token_limit=1_000_000

Dev Agent Record

Implementation Notes

  • Migration 126: Dùng argon2-cffi (from argon2 import PasswordHasher) để hash password. Không dùng bcrypt vì fastapi-users mặc định dùng argon2 với cấu hình chuẩn.
  • Migration 126: Không có cột created_at/updated_at trên bảng user (fastapi-users base không có) — không insert các cột này.
  • Migration 126: searchspaces cần citations_enabled=TRUE vì cột NOT NULL và không có server default.
  • Migration 127: Dùng hoàn toàn raw SQL — không dùng op.create_table, không dùng SQLAlchemy.Enum.
  • SubscriptionRequest model: __allow_unmapped__ = True để tương thích với codebase hiện tại.
  • Admin routes: SubscriptionRequestItem Pydantic schema thêm field user_email (không có trong ORM model, populated thủ công khi query).
  • Frontend: dùng authenticatedFetch từ @/lib/auth-utilsBACKEND_URL từ @/lib/env-config.

Completion Notes

AC 1-3: Migration 126 seed admin user — idempotent, argon2 hashed, full setup. AC 4-7: Admin routes với superuser guard — list/approve/reject subscription requests. AC 8: Frontend /admin/subscription-requests với auth guard và approve/reject UI.

E2E Test Results (2026-04-15)

  • Backend restarted với STRIPE_SECRET_KEY="" → admin-approval mode active.
  • User epic5user@example.com click "Upgrade to Pro" → toast hiển thị đúng.
  • Login admin@surfsense.local/admin/subscription-requests → thấy pending request của epic5user.
  • Click Approve → request biến mất khỏi danh sách.
  • Query DB xác nhận: subscription_status=active, plan_id=pro_monthly, monthly_token_limit=1000000, pages_limit=5000.

File List

  • surfsense_backend/alembic/versions/126_seed_admin_user.py — NEW: admin seed migration (no-op if users exist)
  • surfsense_backend/alembic/versions/127_add_subscription_requests_table.py — NEW: subscription_requests table (raw SQL)
  • surfsense_backend/app/db.py — MODIFIED: SubscriptionRequestStatus enum, SubscriptionRequest model, subscription_requests relationship trên User, values_callable fix trên tất cả SubscriptionStatus enum columns
  • surfsense_backend/app/routes/admin_routes.py — NEW: GET/approve/reject subscription requests, superuser-only
  • surfsense_backend/app/routes/__init__.py — MODIFIED: import và include admin_router
  • surfsense_web/app/admin/subscription-requests/page.tsx — NEW: admin UI page

Change Log

  • 2026-04-15: Implement admin seed migration + admin-approval subscription flow.

Review Findings (2026-04-15)

  • [Review][Patch] Hard-coded default admin password should warn operators [126_seed_admin_user.py:53] — Fixed: Added runtime print warning when ADMIN_PASSWORD env var is not set
  • [Review][Patch] Race condition: concurrent approval lacks row-level lock [admin_routes.py:98] — Fixed: Added .with_for_update() to SubscriptionRequest and User selects in approve endpoint
  • [Review][Patch] N+1 query in list_subscription_requests [admin_routes.py:62] — Fixed: Batch-loaded users with .where(User.id.in_(user_ids)) instead of per-request query