Files
resolutionflow/docs/archive/2026-02-12-admin-invite-user-mgmt.md
chihlasm 350c977eda feat: add procedural flows with intake forms, navigation, and seed templates
Adds a new "procedural" tree type for linear step-by-step project workflows
(domain controller setup, M365 onboarding, VPN config, etc). Includes intake
form builder, two-panel step navigation, variable resolution, procedural
exports, 3 seed templates, and UI rename from "Trees" to "Flows".

Also archives 19 implemented plan docs and creates deferred features backlog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 04:13:52 -05:00

391 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | 190 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 <invites@resolutionflow.com>"`
- 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 190)
`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 178183)
- 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 ~178183) |
| `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 190 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).