# Admin Panel: Invite Codes + User Management Enhancement **Date:** 2026-02-12 **Status:** Proposed — Combined Plan --- ## Summary Enhance admin capabilities to: 1. Create invite codes tied to plans (`free`, `pro`, `team`) with optional trial durations. 2. Send invite emails via Resend (best-effort, non-blocking). 3. Apply invite-assigned plan/trial on registration. 4. Give admins a detailed user management view with subscription, session, and audit context. 5. Support admin subscription actions (change plan, extend/start trial). 6. Auto-downgrade expired trials during authenticated access checks. --- ## Goals - Remove manual invite-code sharing workflow. - Support controlled beta onboarding with plan + trial at invite level. - Enable operational admin workflows for account/subscription lifecycle. - Keep backward compatibility where practical and avoid unsafe breaking changes. --- ## Non-Goals - Stripe billing workflow redesign. - Full historical pagination for user-detail sessions/audits in this iteration. - Rework of account invite (`/accounts/me/invites`) flow. --- ## Key Decisions Locked | Decision | Choice | Rationale | |----------|--------|-----------| | Invite API path | Standardize on `/invites` | Already in use (`router = APIRouter(prefix="/invites")`). Update CLAUDE.md which incorrectly references `/invite-codes`. | | User detail endpoint | Enrich existing `GET /admin/users/{id}` | One endpoint, richer response. No reason for admin to get a "lite" version. | | Invite email matching | Advisory only (no strict enforcement) | The invite code itself is the security gate. Email is for admin tracking. Strict matching creates friction during beta. | | Invite plan/trial application | Applies whenever a valid invite code is provided, even if `REQUIRE_INVITE_CODE=false` | Ensures plan/trial always carries through regardless of registration policy. | | Trial duration bounds | 1–90 days | 90 days covers any realistic beta period. Protects against typos. Admin can always extend after expiry. | | Extend trial behavior | May convert non-trialing subscriptions to `trialing` | Admin should have maximum control. Covers scenarios like forgotten trial assignment or second chances. | | User detail payload | Recent summaries (latest 10 sessions + 10 audit logs) + total counts | Balances useful at-a-glance admin view with response performance. Full history via future paginated endpoints. | --- ## Phase 1: Database Migration (030) **New file:** `backend/alembic/versions/030_enhance_invite_codes.py` (down revision `029`) Add columns to `invite_codes`: - `email`: `String(255)`, nullable, indexed. - `assigned_plan`: `String(50)`, non-null, server default `'free'`. - `trial_duration_days`: `Integer`, nullable. - `email_sent_at`: `DateTime(timezone=True)`, nullable. Database constraints: - `assigned_plan IN ('free', 'pro', 'team')`. - `trial_duration_days IS NULL OR trial_duration_days BETWEEN 1 AND 90`. - Consistency guard: `assigned_plan = 'free'` implies `trial_duration_days IS NULL`. **Update:** `backend/app/models/invite_code.py` - Add mapped columns for all new fields. - Add computed properties: - `has_trial: bool` — `trial_duration_days is not None and > 0` - `email_sent: bool` — `email_sent_at is not None` --- ## Phase 2: Resend Email Integration **New file:** `backend/app/core/email.py` - `EmailService` class with `send_invite_email(to_email, code, plan, trial_days, signup_url) -> bool`. - Returns `False` if `RESEND_API_KEY` not set (graceful degradation). - Catches provider failures, returns `False`, logs warning/error. - Never blocks invite creation (best-effort delivery). **New file:** `backend/app/templates/invite_email.html` - Branded HTML email: monochrome design, ResolutionFlow logo, CTA button. - Shows invite code, plan name, trial duration if applicable, signup link. **Update:** `backend/app/core/config.py` - Add `RESEND_API_KEY: Optional[str] = None` - Add `FROM_EMAIL: str = "ResolutionFlow "` - Add `email_enabled` computed property. **Update:** `backend/requirements.txt` — add `resend` package. **Env vars required:** `RESEND_API_KEY`, `FROM_EMAIL` (has default). **Prerequisite:** DNS records (SPF, DKIM) must be configured in Resend for `resolutionflow.com` domain before production email delivery will work. --- ## Phase 3: Backend Schemas + Endpoints ### 3a. Invite Code Schemas **Update:** `backend/app/schemas/invite_code.py` `InviteCodeCreate` — add fields: - `email: Optional[EmailStr]` - `assigned_plan: Literal['free', 'pro', 'team'] = 'free'` - `trial_duration_days: Optional[int]` (validated 1–90) `InviteCodeResponse` — add fields: - `email`, `assigned_plan`, `trial_duration_days`, `email_sent_at` - Computed flags: `has_trial`, `email_sent` ### 3b. Invite Endpoints **Update:** `backend/app/api/endpoints/invite.py` - `POST /invites` — accept new fields (email, assigned_plan, trial_duration_days). - Create invite with plan/trial/email metadata. - If email provided, attempt send via EmailService. - On send success: set `email_sent_at`. - On send failure: invite still returns 201. - Add audit log entry for invite creation with delivery result. - Keep `GET /invites`, `DELETE /invites/{code}`, `GET /invites/validate/{code}` behavior compatible. ### 3c. Registration Plan Assignment **Update:** `backend/app/api/endpoints/auth.py` (registration endpoint, around lines 178–183) - If invite code is supplied and valid, load it and apply invite plan/trial **regardless of `REQUIRE_INVITE_CODE` setting**. - For non-account-invite registrations: - Create subscription with `plan = invite_code.assigned_plan` (fallback `'free'`). - If `trial_duration_days` is set: - `status = 'trialing'` - `current_period_start = now` - `current_period_end = now + trial_duration_days` - Else: `status = 'active'`. - Preserve existing account-invite join flow behavior. - Mark invite as used after user creation. ### 3d. Admin Subscription + User Detail Endpoints **Update:** `backend/app/api/endpoints/admin.py` Enrich `GET /admin/users/{id}` response to include: - Base user fields (name, email, role, active status). - Account summary (account name, display code). - Subscription summary (plan, status, trial end date). - Recent sessions: latest 10 + total count. - Recent audit logs: latest 10 + total count. - Invite code used summary (code, assigned plan, who created it). Add new endpoints: - `PUT /admin/users/{id}/subscription/plan` — change user's plan. - `PUT /admin/users/{id}/subscription/extend-trial` — add days to trial, or convert to trialing if not already. Both endpoints should create audit log entries. ### 3e. Trial Expiry Check **Update:** `backend/app/api/deps.py` — in `get_current_active_user` - Check account subscription status. - If `status = 'trialing'` and `current_period_end < now`: - Set `plan = 'free'`, `status = 'active'`. - Clear/normalize trial period fields. - Commit before returning user. **Note:** This is a lightweight login-time check. Users with active JWT sessions will retain access until token refresh. Acceptable for beta; revisit if stricter enforcement needed later. --- ## Phase 4: Backend Schema Additions **Check first:** Verify whether `backend/app/schemas/subscription.py` already exists. If it does, extend it. If not, create it. Schemas needed in `backend/app/schemas/subscription.py`: - `SubscriptionPlanUpdate` — for plan change requests. - `ExtendTrialRequest` — for trial extension requests. - `SubscriptionResponse` — for subscription state in responses. **New file:** `backend/app/schemas/user_detail.py` - `AccountSummary` - `SessionSummary` - `AuditLogSummary` - `InviteCodeUsedSummary` - `UserDetailResponse` (superset response for enriched `/admin/users/{id}`) --- ## Phase 5: Frontend Types + API Client **Update:** `frontend/src/types/admin.ts` - Enhanced `InviteCodeResponse` with email, plan, trial, email-sent fields. - New types: `UserDetail`, `SubscriptionDetail`, `SessionSummary`, `AuditLogSummary`, `AccountSummary`. **Update:** `frontend/src/api/admin.ts` - Ensure invite endpoints target `/invites` (not `/invite-codes`). - Enhance `createInviteCode` payload with new fields. - Add: `getUserDetail(userId)`, `updateUserSubscriptionPlan(userId, plan)`, `extendUserTrial(userId, days)`. --- ## Phase 6: Frontend — Enhanced Invite Codes Page **Update:** `frontend/src/pages/admin/InviteCodesPage.tsx` Create form additions: - Email input (optional, validated). - Plan selector dropdown (Free / Pro / Team). - Trial duration input (number of days, shown only when plan ≠ free). Table additions: - "Recipient" column (email or "—"). - "Plan" column with badge. - "Trial" column (days or "—"). - "Email Sent" indicator. Preserve existing create/copy/delete actions and status badges. --- ## Phase 7: Frontend — User Detail Page **New file:** `frontend/src/pages/admin/UserDetailPage.tsx` Sections: - **Header** — name, email, role badges, active status. - **Account & Subscription card** — plan, status, trial end date, account display code. - **Admin Actions card** — Change Role, Change Plan, Extend/Start Trial, Activate/Deactivate (modal-based). - **Recent Sessions tab** — tree name, started, completed, outcome. - **Audit Logs tab** — action, resource, timestamp, expandable details. - **Invite Code card** — code used, plan assigned, who created it. **Update:** `frontend/src/router.tsx` — add route `admin/users/:userId`. **Update:** `frontend/src/pages/admin/UsersPage.tsx` — make user rows clickable to navigate to detail page. Ensure action menu clicks (dropdowns, buttons) don't trigger row navigation. --- ## File Inventory ### Files to Create | File | Phase | |------|-------| | `backend/alembic/versions/030_enhance_invite_codes.py` | 1 | | `backend/app/core/email.py` | 2 | | `backend/app/templates/invite_email.html` | 2 | | `backend/app/schemas/subscription.py` (verify doesn't exist first) | 4 | | `backend/app/schemas/user_detail.py` | 4 | | `frontend/src/pages/admin/UserDetailPage.tsx` | 7 | ### Files to Modify | File | Phase | What Changes | |------|-------|-------------| | `backend/app/models/invite_code.py` | 1 | Add new columns + computed properties | | `backend/app/core/config.py` | 2 | Add RESEND_API_KEY, FROM_EMAIL, email_enabled | | `backend/requirements.txt` | 2 | Add resend package | | `backend/app/schemas/invite_code.py` | 3a | Add email, plan, trial fields to create/response | | `backend/app/api/endpoints/invite.py` | 3b | Accept new fields, send email, audit log | | `backend/app/api/endpoints/auth.py` | 3c | Apply invite plan/trial on registration (lines ~178–183) | | `backend/app/api/endpoints/admin.py` | 3d | Enrich user detail, add subscription endpoints | | `backend/app/api/deps.py` | 3e | Trial expiry check in get_current_active_user | | `frontend/src/types/admin.ts` | 5 | Enhanced invite + new detail types | | `frontend/src/api/admin.ts` | 5 | New API functions, fix invite path | | `frontend/src/pages/admin/InviteCodesPage.tsx` | 6 | Form + table enhancements | | `frontend/src/pages/admin/UsersPage.tsx` | 7 | Clickable rows → detail page | | `frontend/src/router.tsx` | 7 | Add user detail route | ### Also Update (Housekeeping) | File | What Changes | |------|-------------| | `CLAUDE.md` | Fix invite codes endpoint reference from `/invite-codes` to `/invites` | --- ## API / Interface Changes ### Modified Endpoints - `POST /invites` — new request fields: `email`, `assigned_plan`, `trial_duration_days`. - `GET /invites` — new response fields: `email`, `assigned_plan`, `trial_duration_days`, `email_sent_at`, `has_trial`, `email_sent`. - `GET /admin/users/{id}` — enriched response with account/subscription/recent activity details. ### New Endpoints - `PUT /admin/users/{id}/subscription/plan` - `PUT /admin/users/{id}/subscription/extend-trial` --- ## Implementation Order 1. Migration 030 (invite code fields) 2. Model update (invite_code.py) 3. Resend integration (email.py, config.py, template, requirements.txt) 4. Backend schemas (invite_code, subscription, user_detail) 5. Backend API (invite.py, auth.py, admin.py, deps.py) 6. Backend tests 7. Frontend types + API client 8. Frontend invite codes page enhancement 9. Frontend user detail page 10. End-to-end testing --- ## Test Plan ### Backend Tests 1. Invite create with `assigned_plan` + `trial_duration_days` persists correctly. 2. Invite create with email — Resend success sets `email_sent_at`. 3. Invite create with email — Resend failure still returns 201, does not set `email_sent_at`. 4. Registration with invite applies correct subscription plan/status/period fields. 5. Registration with optional invite (`REQUIRE_INVITE_CODE=false`) still applies plan/trial when code provided. 6. Expired trial auto-downgrades on authenticated request. 7. Admin plan update endpoint updates subscription + creates audit log. 8. Admin extend-trial endpoint converts/extends correctly + creates audit log. 9. Enriched `GET /admin/users/{id}` returns expected shape and list-size caps (10 sessions, 10 audit logs). 10. Trial duration validation rejects values outside 1–90 range. 11. Free plan invite rejects trial_duration_days (consistency guard). ### Frontend Verification 1. Create invite with email + plan + trial from admin UI. 2. Confirm invite table renders recipient/plan/trial/email-sent columns. 3. Open user detail from users table (click row). 4. Change plan and extend trial from detail page. 5. Confirm updated values refresh in UI. 6. `cd frontend && npm run build` passes. ### Commands ``` cd backend && pytest --override-ini="addopts=" cd frontend && npm run build ``` --- ## Risks and Mitigations | Risk | Mitigation | |------|-----------| | Endpoint drift (`/invite-codes` vs `/invites`) | Update CLAUDE.md and admin API client. Verify all admin invite calls use `/invites`. | | Subscription side-effects in auth/deps | Centralize trial-expiry logic. Cover with targeted tests. | | Payload growth for user detail | Cap related arrays at 10 items, include total counts. | | Email provider outages | Best-effort send with logging. Invite creation never fails due to email. | | DNS not configured for Resend | Document as prerequisite. Email gracefully degrades when API key missing. | | `subscription.py` may already exist | Verify before creating. Extend if present, create if not. | | JWT session outlives trial expiry | Acceptable for beta. Document as known limitation. | --- ## Rollout 1. Deploy migration and backend changes. 2. Validate admin invite creation and registration path in staging. 3. Deploy frontend with new invite/user-detail UI. 4. Monitor audit logs and invite email delivery behavior post-release. --- ## Assumptions - Existing admin access control (`require_admin`) remains unchanged. - Plan limits for `free/pro/team` are already configured in `plan_limits`. - No mandatory template engine addition is required for email template rendering. - Alembic `env.py` already imports `InviteCode` model (per LESSONS-LEARNED.md).