feat: admin invite codes with plan assignment + user detail page
- Migration 030: add email, assigned_plan, trial_duration_days, email_sent_at
to invite_codes with CHECK constraints
- Resend email integration (graceful degradation when API key not set)
- Invite codes now support plan assignment (free/pro/team) and trial duration (1-90 days)
- Registration applies invite code plan/trial to new subscription
- Auto-downgrade expired trials on authenticated access
- Enriched GET /admin/users/{id} with account, subscription, sessions, audit logs
- New endpoints: PUT /admin/users/{id}/subscription/plan and extend-trial
- Frontend: enhanced invite codes page with email, plan, trial fields
- Frontend: new user detail page at /admin/users/:userId
- Fixed API path drift: /invite-codes -> /invites
- 11 new backend tests, 416 total passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
173
docs/plans/2026-02-11-invite-codes-admin-panel.md
Normal file
173
docs/plans/2026-02-11-invite-codes-admin-panel.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Admin Panel: Invite Codes + User Management Enhancement
|
||||
|
||||
## Context
|
||||
|
||||
The admin panel has basic invite code CRUD and user listing, but lacks:
|
||||
- **Plan assignment on invite codes** — all registrations get "free" plan
|
||||
- **Email delivery** — admin must manually copy/send codes
|
||||
- **Trial duration** — no time-limited plan access for beta testers
|
||||
- **User detail page** — no way to view/manage a user's subscription, activity, or trial
|
||||
|
||||
This change enables the admin to create invite codes tied to specific plans (free/pro/team) with optional trial durations, send branded invite emails via Resend, and manage user subscriptions from a detailed user page.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Database Migration (030)
|
||||
|
||||
**New file:** `backend/alembic/versions/030_enhance_invite_codes.py`
|
||||
|
||||
Add columns to `invite_codes`:
|
||||
- `email` (String(255), nullable, indexed)
|
||||
- `assigned_plan` (String(50), nullable, default `'free'`, CHECK `free/pro/team`)
|
||||
- `trial_duration_days` (Integer, nullable)
|
||||
- `email_sent_at` (DateTime(timezone=True), nullable)
|
||||
|
||||
**Update:** `backend/app/models/invite_code.py` — add fields + `has_trial` and `email_sent` properties
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Resend Email Integration
|
||||
|
||||
**New file:** `backend/app/core/email.py`
|
||||
- `EmailService` class with `send_invite_email(to, code, plan, trial_days)`
|
||||
- Graceful degradation: if `RESEND_API_KEY` not set, log warning, skip sending
|
||||
- Email failure doesn't block invite creation (best-effort)
|
||||
|
||||
**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`, `FROM_EMAIL`, `email_enabled` property
|
||||
**Update:** `backend/requirements.txt` — add `resend`
|
||||
|
||||
**Env vars:** `RESEND_API_KEY`, `FROM_EMAIL=ResolutionFlow <invites@resolutionflow.com>`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Backend API Changes
|
||||
|
||||
### Invite code enhancements
|
||||
|
||||
**Update:** `backend/app/schemas/invite_code.py`
|
||||
- `InviteCodeCreate`: add `email`, `assigned_plan`, `trial_duration_days`
|
||||
- `InviteCodeResponse`: add new fields + computed `has_trial`, `email_sent`
|
||||
|
||||
**Update:** `backend/app/api/endpoints/invite.py`
|
||||
- `create_invite_code`: accept new fields, send email if email provided, set `email_sent_at`, audit log
|
||||
|
||||
### Registration plan assignment
|
||||
|
||||
**Update:** `backend/app/api/endpoints/auth.py` (lines 178-183)
|
||||
- When `invite_code_record` has `assigned_plan`/`trial_duration_days`, apply to new subscription
|
||||
- Set `plan=invite_code_record.assigned_plan`, `status='trialing'` if trial, calculate `current_period_end`
|
||||
|
||||
### Subscription management endpoints
|
||||
|
||||
**Update:** `backend/app/api/endpoints/admin.py`
|
||||
- `PUT /admin/users/{id}/subscription/plan` — change plan
|
||||
- `PUT /admin/users/{id}/subscription/extend-trial` — add days to trial
|
||||
- `GET /admin/users/{id}/detail` — enhanced user detail with account, subscription, sessions, audit logs, invite code used
|
||||
|
||||
**New file:** `backend/app/schemas/subscription.py` — `SubscriptionPlanUpdate`, `ExtendTrialRequest`, `SubscriptionResponse`
|
||||
**New file:** `backend/app/schemas/user_detail.py` — `UserDetailResponse`, `SessionSummary`, `AuditLogSummary`, `AccountSummary`
|
||||
|
||||
### Trial expiry on login (lightweight)
|
||||
|
||||
**Update:** `backend/app/api/deps.py` — in `get_current_active_user`, check if subscription is trialing and expired → auto-downgrade to free
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Frontend Types & API Client
|
||||
|
||||
**Update:** `frontend/src/types/admin.ts`
|
||||
- Enhanced `InviteCodeResponse` with email/plan/trial fields
|
||||
- New: `UserDetail`, `SubscriptionDetail`, `SessionSummary`, `AuditLogSummary`, `AccountSummary`
|
||||
|
||||
**Update:** `frontend/src/api/admin.ts`
|
||||
- Enhanced `createInviteCode` with new fields
|
||||
- New: `getUserDetail`, `updateUserSubscriptionPlan`, `extendUserTrial`
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 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 if plan != free)
|
||||
|
||||
Table additions:
|
||||
- "Recipient" column (email or "—")
|
||||
- "Plan" column with badge
|
||||
- "Trial" column (days or "—")
|
||||
- "Email Sent" indicator
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Frontend — User Detail Page
|
||||
|
||||
**New file:** `frontend/src/pages/admin/UserDetailPage.tsx`
|
||||
|
||||
Sections:
|
||||
1. **Header** — name, email, role badges, active status
|
||||
2. **Account & Subscription card** — plan, status, trial end date, account display code
|
||||
3. **Admin Actions card** — Change Role, Change Plan, Extend Trial, Activate/Deactivate (modal-based)
|
||||
4. **Recent Sessions tab** — tree name, started, completed, outcome
|
||||
5. **Audit Logs tab** — action, resource, timestamp, expandable details
|
||||
6. **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 → navigate to detail page
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
## Files to Create
|
||||
- `backend/alembic/versions/030_enhance_invite_codes.py`
|
||||
- `backend/app/core/email.py`
|
||||
- `backend/app/templates/invite_email.html`
|
||||
- `backend/app/schemas/subscription.py`
|
||||
- `backend/app/schemas/user_detail.py`
|
||||
- `frontend/src/pages/admin/UserDetailPage.tsx`
|
||||
|
||||
## Files to Modify
|
||||
- `backend/app/models/invite_code.py`
|
||||
- `backend/app/schemas/invite_code.py`
|
||||
- `backend/app/api/endpoints/invite.py`
|
||||
- `backend/app/api/endpoints/auth.py` (lines 178-183)
|
||||
- `backend/app/api/endpoints/admin.py`
|
||||
- `backend/app/api/deps.py`
|
||||
- `backend/app/core/config.py`
|
||||
- `backend/requirements.txt`
|
||||
- `frontend/src/types/admin.ts`
|
||||
- `frontend/src/api/admin.ts`
|
||||
- `frontend/src/pages/admin/InviteCodesPage.tsx`
|
||||
- `frontend/src/pages/admin/UsersPage.tsx`
|
||||
- `frontend/src/router.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Backend tests:** Create invite with plan+trial → register with code → verify subscription has correct plan/status/period_end
|
||||
2. **Email test:** Mock Resend, verify template renders, verify email_sent_at set on success
|
||||
3. **Trial expiry:** Create expired trial → login → verify auto-downgrade to free
|
||||
4. **Admin UI:** Create invite with email+plan+trial → verify email sent → register → verify in user detail page → change plan → extend trial
|
||||
5. **Build:** `cd frontend && npm run build` passes
|
||||
6. **Full test suite:** `cd backend && pytest --override-ini="addopts="` passes
|
||||
@@ -0,0 +1,253 @@
|
||||
# Admin Panel: Invite Codes + User Management Enhancement
|
||||
|
||||
Date: 2026-02-12
|
||||
Status: Proposed
|
||||
|
||||
## 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/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
|
||||
- Invite API path standardization: use `/invites` (frontend and backend aligned).
|
||||
- User detail endpoint: enrich existing `GET /admin/users/{id}`.
|
||||
- Invite `email` is advisory only (no strict email-match enforcement at registration).
|
||||
- Invite plan/trial applies whenever a valid invite code is provided, even if `REQUIRE_INVITE_CODE=false`.
|
||||
- Trial duration bounds: `1..90` days.
|
||||
- Extend trial endpoint may convert non-trialing subscriptions to `trialing`.
|
||||
- User detail payload includes recent summaries (latest 10 sessions + latest 10 audit logs) plus total counts.
|
||||
|
||||
## Scope by Phase
|
||||
|
||||
## Phase 1: Database Migration (`030`)
|
||||
Create `backend/alembic/versions/030_enhance_invite_codes.py` (down revision `029`).
|
||||
|
||||
Add 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.
|
||||
|
||||
Constraints:
|
||||
- `assigned_plan IN ('free','pro','team')`.
|
||||
- `trial_duration_days IS NULL OR trial_duration_days BETWEEN 1 AND 90`.
|
||||
- Optional consistency guard: `assigned_plan='free'` implies `trial_duration_days IS NULL`.
|
||||
|
||||
Update model `backend/app/models/invite_code.py`:
|
||||
- Add mapped columns above.
|
||||
- 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
|
||||
Create `backend/app/core/email.py`:
|
||||
- `EmailService.send_invite_email(to_email, code, plan, trial_days, signup_url) -> bool`.
|
||||
- Returns `False` if `RESEND_API_KEY` missing.
|
||||
- Catches provider failures and returns `False` (logs warning/error).
|
||||
- Never blocks invite creation.
|
||||
|
||||
Create `backend/app/templates/invite_email.html`:
|
||||
- Monochrome branded HTML.
|
||||
- Invite code, plan, optional trial text, signup CTA button.
|
||||
|
||||
Update `backend/app/core/config.py`:
|
||||
- `RESEND_API_KEY: Optional[str] = None`
|
||||
- `FROM_EMAIL: str = "ResolutionFlow <invites@resolutionflow.com>"`
|
||||
- `email_enabled` property.
|
||||
|
||||
Update `backend/requirements.txt`:
|
||||
- Add `resend` package.
|
||||
|
||||
## Phase 3: Backend Schemas + Endpoints
|
||||
|
||||
### Invite code schemas
|
||||
Update `backend/app/schemas/invite_code.py`:
|
||||
- `InviteCodeCreate` adds:
|
||||
- `email: Optional[EmailStr]`
|
||||
- `assigned_plan: Literal['free','pro','team'] = 'free'`
|
||||
- `trial_duration_days: Optional[int]` (1..90)
|
||||
- `InviteCodeResponse` adds:
|
||||
- `email`, `assigned_plan`, `trial_duration_days`, `email_sent_at`
|
||||
- computed flags `has_trial`, `email_sent`.
|
||||
|
||||
### Invite endpoints
|
||||
Update `backend/app/api/endpoints/invite.py`:
|
||||
- `POST /invites` accepts new fields.
|
||||
- Creates invite with plan/trial/email metadata.
|
||||
- If email provided, attempts send:
|
||||
- on success: set `email_sent_at`.
|
||||
- on failure: invite still returns 201.
|
||||
- Add audit log for invite creation with delivery result.
|
||||
- Keep `GET /invites`, `DELETE /invites/{code}`, `GET /invites/validate/{code}` behavior compatible.
|
||||
|
||||
### Registration plan assignment
|
||||
Update `backend/app/api/endpoints/auth.py`:
|
||||
- If invite code is supplied and valid, load it and apply invite plan/trial regardless of `REQUIRE_INVITE_CODE`.
|
||||
- For non-account-invite registrations:
|
||||
- create subscription `plan=invite_code.assigned_plan` (fallback `free`).
|
||||
- if `trial_duration_days` set:
|
||||
- `status='trialing'`
|
||||
- `current_period_start=now`
|
||||
- `current_period_end=now + trial_duration_days`.
|
||||
- else `status='active'`.
|
||||
- Preserve account-invite join flow behavior.
|
||||
- Mark invite as used post user creation.
|
||||
|
||||
### Admin subscription + detail endpoints
|
||||
Update `backend/app/api/endpoints/admin.py`:
|
||||
- Enrich `GET /admin/users/{id}` response:
|
||||
- base user fields
|
||||
- account summary
|
||||
- subscription summary
|
||||
- recent sessions (10) + total count
|
||||
- recent audit logs (10) + total count
|
||||
- invite code used summary
|
||||
- Add:
|
||||
- `PUT /admin/users/{id}/subscription/plan`
|
||||
- `PUT /admin/users/{id}/subscription/extend-trial`
|
||||
|
||||
### Trial expiry check
|
||||
Update `backend/app/api/deps.py`:
|
||||
- In `get_current_active_user`, check account subscription.
|
||||
- If `status='trialing'` and expired, auto-downgrade:
|
||||
- `plan='free'`, `status='active'`
|
||||
- clear/normalize trial period fields
|
||||
- commit before returning user.
|
||||
|
||||
## Phase 4: Backend Schema Additions
|
||||
Use existing file `backend/app/schemas/subscription.py` (do not duplicate):
|
||||
- Add `SubscriptionPlanUpdate`.
|
||||
- Add `ExtendTrialRequest`.
|
||||
- Keep/extend `SubscriptionResponse` as needed.
|
||||
|
||||
Create `backend/app/schemas/user_detail.py`:
|
||||
- `AccountSummary`
|
||||
- `SessionSummary`
|
||||
- `AuditLogSummary`
|
||||
- `InviteCodeUsedSummary`
|
||||
- `UserDetailResponse` (superset for enriched `/admin/users/{id}`).
|
||||
|
||||
## Phase 5: Frontend Types + API Client
|
||||
Update `frontend/src/types/admin.ts`:
|
||||
- Invite response fields for email/plan/trial/email-sent metadata.
|
||||
- New detail types:
|
||||
- `UserDetail`
|
||||
- `SubscriptionDetail`
|
||||
- `SessionSummary`
|
||||
- `AuditLogSummary`
|
||||
- `AccountSummary`.
|
||||
|
||||
Update `frontend/src/api/admin.ts`:
|
||||
- Switch invite endpoints to `/invites`.
|
||||
- Enhance `createInviteCode` payload.
|
||||
- Add:
|
||||
- `getUserDetail(userId)`
|
||||
- `updateUserSubscriptionPlan(userId, plan)`
|
||||
- `extendUserTrial(userId, days)`.
|
||||
|
||||
## Phase 6: Frontend Invite Codes Page
|
||||
Update `frontend/src/pages/admin/InviteCodesPage.tsx`:
|
||||
- Create form fields:
|
||||
- optional email
|
||||
- plan selector (Free/Pro/Team)
|
||||
- trial days input when plan != free
|
||||
- Table additions:
|
||||
- recipient
|
||||
- plan badge
|
||||
- trial column
|
||||
- email sent indicator
|
||||
- Preserve existing create/copy/delete actions and status badges.
|
||||
|
||||
## Phase 7: Frontend User Detail Page
|
||||
Create `frontend/src/pages/admin/UserDetailPage.tsx`:
|
||||
- Header: name/email/role/active.
|
||||
- Account & subscription card.
|
||||
- Admin actions:
|
||||
- change role
|
||||
- change plan
|
||||
- extend/start trial
|
||||
- activate/deactivate
|
||||
- Tabs:
|
||||
- recent sessions
|
||||
- audit logs
|
||||
- Invite code card:
|
||||
- code, assigned plan, creator.
|
||||
|
||||
Update `frontend/src/router.tsx`:
|
||||
- Add route `admin/users/:userId`.
|
||||
|
||||
Update `frontend/src/pages/admin/UsersPage.tsx`:
|
||||
- Make rows navigate to detail.
|
||||
- Ensure action menu clicks do not trigger row navigation.
|
||||
|
||||
## API / Interface Changes
|
||||
|
||||
### Modified
|
||||
- `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.
|
||||
|
||||
### Added
|
||||
- `PUT /admin/users/{id}/subscription/plan`
|
||||
- `PUT /admin/users/{id}/subscription/extend-trial`
|
||||
|
||||
## 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`.
|
||||
- Resend failure still returns 201 and does not set `email_sent_at`.
|
||||
3. Registration with invite applies correct subscription plan/status/period fields.
|
||||
4. Registration with optional invite (`REQUIRE_INVITE_CODE=false`) still applies plan/trial.
|
||||
5. Expired trial auto-downgrades on authenticated request.
|
||||
6. Admin plan update endpoint updates subscription + audit logs.
|
||||
7. Admin extend-trial endpoint converts/extends correctly + audit logs.
|
||||
8. Enriched `GET /admin/users/{id}` returns expected shape and list-size caps.
|
||||
|
||||
## Frontend verification
|
||||
1. Create invite with email + plan + trial from admin UI.
|
||||
2. Confirm invite table renders recipient/plan/trial/email-sent.
|
||||
3. Open user detail from users table.
|
||||
4. Change plan and extend trial from detail page.
|
||||
5. Confirm updated values refresh in UI.
|
||||
6. `npm run build` passes.
|
||||
|
||||
## Commands
|
||||
- `cd backend && pytest --override-ini="addopts="`
|
||||
- `cd frontend && npm run build`
|
||||
|
||||
## Risks and Mitigations
|
||||
- Endpoint drift (`/invite-codes` vs `/invites`): update admin API client and validate all admin invite calls.
|
||||
- Subscription side-effects in auth/deps: centralize trial-expiry logic and cover with tests.
|
||||
- Payload growth for user detail: cap related arrays at 10 and include totals.
|
||||
- Email provider outages: best-effort send with logging, no invite creation failure.
|
||||
|
||||
## 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 this email template rendering path.
|
||||
390
docs/plans/2026-02-12-admin-invite-user-mgmt.md
Normal file
390
docs/plans/2026-02-12-admin-invite-user-mgmt.md
Normal file
@@ -0,0 +1,390 @@
|
||||
# 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 <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 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).
|
||||
Reference in New Issue
Block a user