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>
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
- Khi chạy
alembic upgrade headtrê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 varsADMIN_EMAIL/ADMIN_PASSWORD. - Admin được seed với
is_superuser=TRUE,subscription_status='active',plan_id='pro_yearly',monthly_token_limit=1_000_000,pages_limit=5000và có đủ search space, roles, membership, và default prompts. - Migration seed là idempotent: nếu đã có bất kỳ user nào trong DB thì bỏ qua, không insert lại.
- Superuser có thể xem danh sách pending subscription requests tại
GET /api/v1/admin/subscription-requests. - 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). - Superuser có thể reject request:
POST /api/v1/admin/subscription-requests/{id}/reject→ request bị đánh dấu rejected. - Non-superuser bị từ chối với HTTP 403 khi truy cập các endpoint admin.
- Frontend tại
/admin/subscription-requestshiển thị bảng pending requests với nút Approve / Reject; chuyển hướng về/loginnế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 khiSELECT 1 FROM "user" LIMIT 1trả 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ớiON CONFLICT DO NOTHING. - Subtask 1.6: Downgrade là no-op (không xóa users).
- Subtask 1.1: Tạo migration
-
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 subscriptionrequeststatustrước khiCREATE TYPE ... AS ENUM ('pending', 'approved', 'rejected'). - Subtask 2.3: Tạo bảng
subscription_requestsvớ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.
- Subtask 2.1: Tạo migration
-
Task 3: SubscriptionRequest Model & ORM
- Subtask 3.1: Thêm
SubscriptionRequestStatus(StrEnum)enum vàodb.py— values:PENDING="pending",APPROVED="approved",REJECTED="rejected". - Subtask 3.2: Thêm
SubscriptionRequest(Base)model sau classPagePurchasetrongdb.py. - Subtask 3.3: Thêm
values_callable=lambda x: [e.value for e in x]vào tất cảSQLAlchemyEnum(SubscriptionStatus)vàSQLAlchemyEnum(SubscriptionRequestStatus)— bắt buộc để ORM map DB lowercase values thay vì enum member names uppercase. - Subtask 3.4: Thêm relationship
subscription_requestsvào cả hai nhánh User model (LOCAL và Google OAuth).
- Subtask 3.1: Thêm
-
Task 4: Admin Routes Backend
- Subtask 4.1: Tạo
surfsense_backend/app/routes/admin_routes.pyvớiAPIRouter(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— setstatus=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): setsubscription_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— setstatus=REJECTED. - Subtask 4.6: Đăng ký router trong
surfsense_backend/app/routes/__init__.pyvàapp.py.
- Subtask 4.1: Tạo
-
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.
- Subtask 5.1: Tạo
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_callable → LookupError: '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)
- Register user → Login →
/pricing→ "Upgrade to Pro" → toast "Subscription request submitted" - Login admin (
admin@surfsense.local/Admin@SurfSense1) →/admin/subscription-requests→ Approve - 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ùngbcryptvì 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_attrên bảnguser(fastapi-users base không có) — không insert các cột này. - Migration 126:
searchspacescầncitations_enabled=TRUEvì 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ùngSQLAlchemy.Enum. SubscriptionRequestmodel:__allow_unmapped__ = Trueđể tương thích với codebase hiện tại.- Admin routes:
SubscriptionRequestItemPydantic schema thêm fielduser_email(không có trong ORM model, populated thủ công khi query). - Frontend: dùng
authenticatedFetchtừ@/lib/auth-utilsvàBACKEND_URLtừ@/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.comclick "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:SubscriptionRequestStatusenum,SubscriptionRequestmodel,subscription_requestsrelationship trên User,values_callablefix trên tất cảSubscriptionStatusenum columnssurfsense_backend/app/routes/admin_routes.py— NEW: GET/approve/reject subscription requests, superuser-onlysurfsense_backend/app/routes/__init__.py— MODIFIED: import và includeadmin_routersurfsense_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