Covers Stripe billing flow, provisioning pipeline, activation magic links, onboarding checklist, refresh tokens, MFA, SSO, passkeys, env var reference, manual intervention checklist, and full API endpoint reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
577 lines
23 KiB
Markdown
577 lines
23 KiB
Markdown
# HOA LedgerIQ -- Payment, Onboarding & Authentication Guide
|
|
|
|
> **Version:** 2026.03.17
|
|
> **Last updated:** March 17, 2026
|
|
> **Migration:** `db/migrations/015-saas-onboarding-auth.sql`
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [High-Level Flow](#1-high-level-flow)
|
|
2. [Stripe Billing & Checkout](#2-stripe-billing--checkout)
|
|
3. [Provisioning Pipeline](#3-provisioning-pipeline)
|
|
4. [Account Activation (Magic Link)](#4-account-activation-magic-link)
|
|
5. [Guided Onboarding Checklist](#5-guided-onboarding-checklist)
|
|
6. [Authentication & Sessions](#6-authentication--sessions)
|
|
7. [Multi-Factor Authentication (TOTP)](#7-multi-factor-authentication-totp)
|
|
8. [Single Sign-On (SSO)](#8-single-sign-on-sso)
|
|
9. [Passkeys (WebAuthn)](#9-passkeys-webauthn)
|
|
10. [Environment Variables Reference](#10-environment-variables-reference)
|
|
11. [Manual Intervention & Ops Tasks](#11-manual-intervention--ops-tasks)
|
|
12. [What's Stubbed vs. Production-Ready](#12-whats-stubbed-vs-production-ready)
|
|
13. [API Endpoint Reference](#13-api-endpoint-reference)
|
|
|
|
---
|
|
|
|
## 1. High-Level Flow
|
|
|
|
```
|
|
Visitor hits /pricing
|
|
|
|
|
v
|
|
Selects a plan (Starter $29 / Professional $79 / Enterprise $199)
|
|
|
|
|
v
|
|
POST /api/billing/create-checkout-session
|
|
|
|
|
v
|
|
Redirect to Stripe Checkout (hosted by Stripe)
|
|
|
|
|
v
|
|
Stripe fires `checkout.session.completed` webhook
|
|
|
|
|
v
|
|
Backend provisions: org -> schema -> user -> invite token -> "email"
|
|
|
|
|
v
|
|
Frontend polls GET /api/billing/status?session_id=xxx
|
|
| (OnboardingPendingPage polls every 3s)
|
|
v
|
|
Status returns "active" -> user is redirected to /login
|
|
|
|
|
v
|
|
User clicks activation link from "email" (logged to console + DB)
|
|
|
|
|
v
|
|
GET /activate?token=xxx -> validates token
|
|
POST /activate -> sets password + name, issues session
|
|
|
|
|
v
|
|
Redirect to /onboarding (4-step guided wizard)
|
|
|
|
|
v
|
|
Dashboard
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Stripe Billing & Checkout
|
|
|
|
### Plans
|
|
|
|
| Plan ID | Name | Price | Unit Limit |
|
|
|---------------|--------------|---------|------------|
|
|
| `starter` | Starter | $29/mo | 50 units |
|
|
| `professional` | Professional | $79/mo | 200 units |
|
|
| `enterprise` | Enterprise | $199/mo | Unlimited |
|
|
|
|
### Checkout Flow
|
|
|
|
1. **Frontend** (`PricingPage.tsx`): User enters email + business name, selects a plan.
|
|
2. **API call**: `POST /api/billing/create-checkout-session`
|
|
- Body: `{ planId, email?, businessName? }`
|
|
- Returns: `{ url }` (Stripe hosted checkout URL)
|
|
- No auth required.
|
|
3. **Redirect**: Frontend does `window.location.href = url` to send user to Stripe.
|
|
4. **On success**: Stripe redirects to `/onboarding/pending?session_id={CHECKOUT_SESSION_ID}`.
|
|
5. **On cancel**: Stripe redirects back to `/pricing`.
|
|
|
|
### Webhook Events Handled
|
|
|
|
The webhook endpoint is `POST /api/webhooks/stripe`.
|
|
|
|
| Event | Action |
|
|
|-------|--------|
|
|
| `checkout.session.completed` | Triggers full provisioning pipeline |
|
|
| `invoice.payment_succeeded` | Sets org status to `active` (handles reactivation after failed payment) |
|
|
| `invoice.payment_failed` | Sends payment-failed "email" (stubbed) |
|
|
| `customer.subscription.deleted` | Sets org status to `archived` |
|
|
|
|
All webhook events are deduplicated via the `shared.stripe_events` table (idempotency by Stripe event ID).
|
|
|
|
### Stripe Customer Portal
|
|
|
|
`POST /api/billing/portal` (auth required) -- creates a Stripe Customer Portal session for managing subscription/payment methods. **Note: currently throws "not implemented" -- needs org-context customer ID lookup.**
|
|
|
|
---
|
|
|
|
## 3. Provisioning Pipeline
|
|
|
|
When `checkout.session.completed` fires, the backend runs **inline provisioning** (no background queue):
|
|
|
|
1. **Create organization** in `shared.organizations` with:
|
|
- `name` = business name from checkout metadata
|
|
- `schema_name` = `tenant_{random_12_chars}`
|
|
- `status` = `active`
|
|
- `plan_level` = selected plan
|
|
- `stripe_customer_id` + `stripe_subscription_id`
|
|
- Uses `ON CONFLICT (stripe_customer_id)` for idempotency
|
|
|
|
2. **Create tenant schema** via `TenantSchemaService.createTenantSchema()`:
|
|
- Runs the full tenant DDL (accounts, journal entries, etc.)
|
|
- Skips if schema already exists
|
|
|
|
3. **Create or find user** in `shared.users` by email:
|
|
- New users are created with `is_email_verified = false` and no password
|
|
- Existing users are reused (linked to new org)
|
|
|
|
4. **Create membership** in `shared.user_organizations`:
|
|
- Role: `president`
|
|
- Idempotent via `ON CONFLICT DO NOTHING`
|
|
|
|
5. **Generate invite token** (JWT signed with `INVITE_TOKEN_SECRET`, 72-hour expiry):
|
|
- SHA-256 hash stored in `shared.invite_tokens`
|
|
- Raw token used in activation URL
|
|
|
|
6. **Send activation "email"** (stubbed -- see section 12):
|
|
- Logged to console and `shared.email_log` table
|
|
- Contains activation URL: `{APP_URL}/activate?token={jwt}`
|
|
|
|
7. **Initialize onboarding** progress row in `shared.onboarding_progress`
|
|
|
|
### Provisioning Status Polling
|
|
|
|
`GET /api/billing/status?session_id=xxx` (no auth required)
|
|
|
|
Returns: `{ status }` where status is one of:
|
|
- `not_configured` -- Stripe not set up
|
|
- `pending` -- no customer ID yet
|
|
- `provisioning` -- org exists but not active yet
|
|
- `active` -- ready to go
|
|
|
|
The `OnboardingPendingPage` polls this every 3 seconds and redirects to `/login` once active.
|
|
|
|
---
|
|
|
|
## 4. Account Activation (Magic Link)
|
|
|
|
### Validate Token
|
|
|
|
`GET /api/auth/activate?token=xxx` (no auth required)
|
|
|
|
- Verifies JWT signature (using `INVITE_TOKEN_SECRET`)
|
|
- Checks `shared.invite_tokens` for existence, expiration, and prior use
|
|
- Returns: `{ valid, email, orgName, orgId, userId }`
|
|
|
|
### Activate Account
|
|
|
|
`POST /api/auth/activate` (no auth required)
|
|
|
|
- Body: `{ token, password, fullName }`
|
|
- Password must be >= 8 characters
|
|
- Sets `password_hash`, `first_name`, `last_name`, `is_email_verified = true`
|
|
- Marks invite token as used (`used_at = NOW()`)
|
|
- Issues full session (access token + refresh token cookie)
|
|
- Frontend redirects to `/onboarding`
|
|
|
|
### Frontend (ActivatePage.tsx)
|
|
|
|
- Validates token on mount
|
|
- Shows password setup form with strength indicator (color-coded bar)
|
|
- On success: stores auth in Zustand and navigates to `/onboarding`
|
|
|
|
---
|
|
|
|
## 5. Guided Onboarding Checklist
|
|
|
|
### Required Steps
|
|
|
|
| Step Key | UI Label | Description |
|
|
|-----------------|----------------|-------------|
|
|
| `profile` | Profile | Set up user profile |
|
|
| `workspace` | Workspace | Configure organization settings |
|
|
| `invite_member` | Invite Member | Invite at least one team member |
|
|
| `first_workflow` | First Account | Create the first chart-of-accounts entry |
|
|
|
|
### API
|
|
|
|
- `GET /api/onboarding/progress` (auth required): Returns `{ completedSteps[], completedAt, requiredSteps[] }`
|
|
- `PATCH /api/onboarding/progress` (auth required): Body `{ step }` -- marks a step complete
|
|
|
|
Steps are stored as a PostgreSQL text array. When all 4 required steps are complete, `completed_at` is set. Users can skip onboarding via a "Finish Later" button (navigates to dashboard).
|
|
|
|
### Frontend (OnboardingPage.tsx)
|
|
|
|
- Mantine Stepper with 4 steps
|
|
- Each step calls `PATCH /onboarding/progress` on completion
|
|
- Celebration screen shown when all steps are done
|
|
|
|
---
|
|
|
|
## 6. Authentication & Sessions
|
|
|
|
### Token Architecture
|
|
|
|
| Token | Type | Lifetime | Storage |
|
|
|-------|------|----------|---------|
|
|
| Access token | JWT | 1 hour | Frontend Zustand store (memory/localStorage) |
|
|
| Refresh token | Opaque (base64url, 64 bytes) | 30 days | httpOnly cookie (`ledgeriq_rt`) |
|
|
| MFA challenge | JWT | 5 minutes | Frontend state (in-memory only) |
|
|
| Invite/activation | JWT | 72 hours | URL query parameter |
|
|
|
|
### Refresh Token Flow
|
|
|
|
1. Access token expires (401 from any API call)
|
|
2. Axios interceptor catches 401, calls `POST /api/auth/refresh`
|
|
3. Refresh token is sent automatically via httpOnly cookie
|
|
4. Server validates token hash in `shared.refresh_tokens` table
|
|
5. New access token issued (refresh token is NOT rotated)
|
|
6. Original failed request is replayed with new token
|
|
7. Concurrent requests are queued during refresh (no thundering herd)
|
|
|
|
### Cookie Configuration
|
|
|
|
- Name: `ledgeriq_rt`
|
|
- Path: `/api/auth`
|
|
- httpOnly: `true`
|
|
- Secure: `true` in production, `false` in dev
|
|
- SameSite: `strict`
|
|
- Max-Age: 30 days
|
|
|
|
### Session Endpoints
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/auth/login` | No | Email + password login |
|
|
| `POST` | `/api/auth/register` | No | Create account |
|
|
| `POST` | `/api/auth/refresh` | Cookie | Refresh access token |
|
|
| `POST` | `/api/auth/logout` | Cookie | Revoke current refresh token |
|
|
| `POST` | `/api/auth/logout-everywhere` | JWT | Revoke all user sessions |
|
|
| `POST` | `/api/auth/switch-org` | JWT | Switch org context (new tokens) |
|
|
|
|
---
|
|
|
|
## 7. Multi-Factor Authentication (TOTP)
|
|
|
|
### Setup Flow
|
|
|
|
1. User goes to Settings > Security > Two-Factor Auth tab
|
|
2. `POST /api/auth/mfa/setup` -- returns `{ qrCodeDataUrl, secret, uri }`
|
|
3. User scans QR code in authenticator app (Google Authenticator, Authy, etc.)
|
|
4. User enters 6-digit code to confirm
|
|
5. `POST /api/auth/mfa/enable` with `{ token }` -- returns `{ recoveryCodes[] }`
|
|
6. **User must save their 10 recovery codes** (displayed once, bcrypt-hashed in DB)
|
|
|
|
### Login with MFA
|
|
|
|
1. `POST /api/auth/login` returns `{ mfaRequired: true, mfaToken }` instead of session
|
|
2. Frontend shows 6-digit PIN input (or recovery code input)
|
|
3. `POST /api/auth/mfa/verify` with `{ mfaToken, token, useRecovery? }`
|
|
4. On success: full session issued (access token + refresh cookie)
|
|
|
|
### Recovery Codes
|
|
|
|
- 10 codes generated on MFA enable
|
|
- Each code is single-use (removed from array after verification)
|
|
- Codes are bcrypt-hashed in `shared.users.recovery_codes` (JSON array)
|
|
|
|
### MFA Endpoints
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/auth/mfa/setup` | JWT | Generate QR + secret |
|
|
| `POST` | `/api/auth/mfa/enable` | JWT | Verify code and enable MFA |
|
|
| `POST` | `/api/auth/mfa/verify` | No (uses mfaToken) | Verify during login |
|
|
| `POST` | `/api/auth/mfa/disable` | JWT | Disable MFA (requires password) |
|
|
| `GET` | `/api/auth/mfa/status` | JWT | Check if MFA is enabled |
|
|
|
|
### Tech Stack
|
|
|
|
- Library: `otplib` v4 (`generateSecret`, `generateURI`, `verifySync`)
|
|
- QR codes: `qrcode` package (data URL output)
|
|
- Recovery codes: `crypto.randomBytes` + `bcryptjs`
|
|
|
|
---
|
|
|
|
## 8. Single Sign-On (SSO)
|
|
|
|
### Supported Providers
|
|
|
|
| Provider | Library | Env Vars Required |
|
|
|----------|---------|-------------------|
|
|
| Google | `passport-google-oauth20` | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_CALLBACK_URL` |
|
|
| Microsoft/Azure AD | `passport-azure-ad` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`, `AZURE_CALLBACK_URL` |
|
|
|
|
SSO providers are **conditionally loaded** -- they only appear on the login page if their env vars are configured. The `GET /api/auth/sso/providers` endpoint returns `{ google: boolean, azure: boolean }`.
|
|
|
|
### SSO Login Flow
|
|
|
|
1. Frontend redirects to `/api/auth/google` or `/api/auth/azure`
|
|
2. Passport handles OAuth redirect to provider
|
|
3. Provider redirects back to `/api/auth/{provider}/callback`
|
|
4. Backend creates or links user via `SsoService.findOrCreateSsoUser()`
|
|
5. Session tokens issued, redirect to `/sso-callback?token={accessToken}`
|
|
|
|
### Account Linking
|
|
|
|
- SSO fields stored on `shared.users`: `sso_provider`, `sso_id`
|
|
- If email matches existing user, SSO is auto-linked on first login
|
|
- Users can unlink: `DELETE /api/auth/sso/unlink/:provider`
|
|
|
|
### SSO Endpoints
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `GET` | `/api/auth/sso/providers` | No | List configured providers |
|
|
| `GET` | `/api/auth/google/callback` | No (OAuth) | Google callback |
|
|
| `GET` | `/api/auth/azure/callback` | No (OAuth) | Azure callback |
|
|
| `DELETE` | `/api/auth/sso/unlink/:provider` | JWT | Unlink SSO provider |
|
|
|
|
---
|
|
|
|
## 9. Passkeys (WebAuthn)
|
|
|
|
### Registration Flow (authenticated user)
|
|
|
|
1. `POST /api/auth/passkeys/register-options` -- returns WebAuthn creation options
|
|
2. Browser `navigator.credentials.create()` via `@simplewebauthn/browser`
|
|
3. `POST /api/auth/passkeys/register` with `{ response, deviceName? }`
|
|
4. Credential stored in `shared.user_passkeys`
|
|
|
|
### Login Flow (unauthenticated)
|
|
|
|
1. `POST /api/auth/passkeys/login-options` with `{ email? }` -- returns assertion options
|
|
2. Browser `navigator.credentials.get()` via `@simplewebauthn/browser`
|
|
3. `POST /api/auth/passkeys/login` with `{ response, challenge }`
|
|
4. Full session issued on success
|
|
|
|
### Passkey Endpoints
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/auth/passkeys/register-options` | JWT | Get registration options |
|
|
| `POST` | `/api/auth/passkeys/register` | JWT | Complete registration |
|
|
| `POST` | `/api/auth/passkeys/login-options` | No | Get authentication options |
|
|
| `POST` | `/api/auth/passkeys/login` | No | Authenticate with passkey |
|
|
| `GET` | `/api/auth/passkeys` | JWT | List registered passkeys |
|
|
| `DELETE` | `/api/auth/passkeys/:id` | JWT | Remove a passkey |
|
|
|
|
### Configuration
|
|
|
|
- `WEBAUTHN_RP_ID` -- Relying party ID (e.g., `localhost` for dev, `yourdomain.com` for prod)
|
|
- `WEBAUTHN_RP_ORIGIN` -- Expected origin (e.g., `http://localhost` or `https://yourdomain.com`)
|
|
- Passkey removal is blocked if the user has no password and no other passkeys (prevents lockout)
|
|
|
|
---
|
|
|
|
## 10. Environment Variables Reference
|
|
|
|
### Stripe (Required for billing)
|
|
|
|
| Variable | Example | Description |
|
|
|----------|---------|-------------|
|
|
| `STRIPE_SECRET_KEY` | `sk_test_...` | Stripe secret key. Must NOT contain "placeholder" to activate. |
|
|
| `STRIPE_WEBHOOK_SECRET` | `whsec_...` | Webhook endpoint signing secret |
|
|
| `STRIPE_STARTER_PRICE_ID` | `price_...` | Stripe Price ID for Starter plan |
|
|
| `STRIPE_PROFESSIONAL_PRICE_ID` | `price_...` | Stripe Price ID for Professional plan |
|
|
| `STRIPE_ENTERPRISE_PRICE_ID` | `price_...` | Stripe Price ID for Enterprise plan |
|
|
|
|
### SSO (Optional -- features hidden when not set)
|
|
|
|
| Variable | Example | Description |
|
|
|----------|---------|-------------|
|
|
| `GOOGLE_CLIENT_ID` | `xxx.apps.googleusercontent.com` | Google OAuth client ID |
|
|
| `GOOGLE_CLIENT_SECRET` | `GOCSPX-...` | Google OAuth client secret |
|
|
| `GOOGLE_CALLBACK_URL` | `http://localhost/api/auth/google/callback` | OAuth redirect URI |
|
|
| `AZURE_CLIENT_ID` | `uuid` | Azure AD application (client) ID |
|
|
| `AZURE_CLIENT_SECRET` | `...` | Azure AD client secret |
|
|
| `AZURE_TENANT_ID` | `uuid` | Azure AD tenant (directory) ID |
|
|
| `AZURE_CALLBACK_URL` | `http://localhost/api/auth/azure/callback` | OAuth redirect URI |
|
|
|
|
### WebAuthn / Passkeys
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `WEBAUTHN_RP_ID` | `localhost` | Relying party identifier (your domain) |
|
|
| `WEBAUTHN_RP_ORIGIN` | `http://localhost` | Expected browser origin |
|
|
|
|
### Other
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `INVITE_TOKEN_SECRET` | `dev-invite-secret` | Secret for signing invite/activation JWTs. **Change in production.** |
|
|
| `APP_URL` | `http://localhost` | Base URL for generated links (activation emails, Stripe redirects) |
|
|
|
|
---
|
|
|
|
## 11. Manual Intervention & Ops Tasks
|
|
|
|
### Before Going Live
|
|
|
|
1. **Set up Stripe products and prices** in the Stripe Dashboard:
|
|
- Create 3 products (Starter, Professional, Enterprise)
|
|
- Create monthly recurring prices for each
|
|
- Copy the Price IDs into `STRIPE_STARTER_PRICE_ID`, etc.
|
|
|
|
2. **Configure the Stripe webhook** in the Stripe Dashboard:
|
|
- Endpoint URL: `https://yourdomain.com/api/webhooks/stripe`
|
|
- Events to subscribe: `checkout.session.completed`, `invoice.payment_succeeded`, `invoice.payment_failed`, `customer.subscription.deleted`
|
|
- Copy the webhook signing secret to `STRIPE_WEBHOOK_SECRET`
|
|
|
|
3. **Replace the email stub** with a real provider:
|
|
- `backend/src/modules/email/email.service.ts` currently logs to console + DB
|
|
- Swap in Resend, SendGrid, SES, or your preferred provider
|
|
- The four email methods to implement: `sendActivationEmail`, `sendWelcomeEmail`, `sendPaymentFailedEmail`, `sendInviteMemberEmail`
|
|
|
|
4. **Set production secrets**:
|
|
- `INVITE_TOKEN_SECRET` -- use a strong random string (not `dev-invite-secret`)
|
|
- `JWT_SECRET` -- already required, verify it's strong
|
|
- `WEBAUTHN_RP_ID` and `WEBAUTHN_RP_ORIGIN` -- set to your production domain
|
|
|
|
5. **Configure SSO providers** (if desired):
|
|
- Register apps in Google Cloud Console and/or Azure AD
|
|
- Set the callback URLs to your production domain
|
|
- Add client IDs and secrets to env vars
|
|
|
|
6. **Set up the Stripe Customer Portal** in Stripe Dashboard:
|
|
- Configure allowed actions (cancel, upgrade/downgrade, payment method updates)
|
|
- The `/api/billing/portal` endpoint needs the org-context customer ID lookup completed
|
|
|
|
### Ongoing Ops
|
|
|
|
7. **Refresh token cleanup**: The `RefreshTokenService.cleanupExpired()` method deletes tokens that have been expired or revoked for 7+ days. **This is not currently called on a schedule.** Options:
|
|
- Add a cron job / scheduled task that calls it periodically
|
|
- Or add a NestJS `@Cron()` decorator (requires `@nestjs/schedule`)
|
|
|
|
8. **Monitor `shared.email_log`**: While email is stubbed, activation URLs are only visible in:
|
|
- Backend console logs (look for lines starting with `EMAIL STUB`)
|
|
- The `shared.email_log` table (query: `SELECT * FROM shared.email_log ORDER BY sent_at DESC`)
|
|
|
|
9. **Finding activation URLs manually** (dev/testing):
|
|
```sql
|
|
SELECT to_email, metadata->>'activationUrl' AS url, sent_at
|
|
FROM shared.email_log
|
|
WHERE template = 'activation'
|
|
ORDER BY sent_at DESC
|
|
LIMIT 10;
|
|
```
|
|
|
|
10. **Resend an activation email**: `POST /api/auth/resend-activation` with `{ email }` is stubbed (always returns success). To manually generate a new token:
|
|
```sql
|
|
-- Find the user and org
|
|
SELECT u.id AS user_id, uo.organization_id
|
|
FROM shared.users u
|
|
JOIN shared.user_organizations uo ON uo.user_id = u.id
|
|
WHERE u.email = 'user@example.com';
|
|
```
|
|
Then call `authService.generateInviteToken(userId, orgId, email)` or trigger a fresh checkout.
|
|
|
|
11. **Deprovisioning / cancellation**: When a Stripe subscription is deleted, the org is set to `archived`. Archived orgs:
|
|
- Block login (users see "Your organization has been suspended")
|
|
- Block API access (403 on org-scoped endpoints)
|
|
- Data is preserved (schema is NOT deleted)
|
|
- To restore: update `status` back to `active` in `shared.organizations`
|
|
|
|
---
|
|
|
|
## 12. What's Stubbed vs. Production-Ready
|
|
|
|
| Component | Status | Notes |
|
|
|-----------|--------|-------|
|
|
| Stripe Checkout | **Ready** (test mode) | Switch to live keys for production |
|
|
| Stripe Webhooks | **Ready** | Signature verification, idempotency, event dispatch all implemented |
|
|
| Stripe Customer Portal | **Stubbed** | Endpoint exists but needs org-context customer ID lookup |
|
|
| Provisioning (org + schema + user) | **Ready** | Inline (synchronous). Consider BullMQ queue for production scale. |
|
|
| Email service | **Stubbed** | Logs to console + `shared.email_log`. Replace with real SMTP/API provider. |
|
|
| Activation (magic link) | **Ready** | Works end-to-end (token generation, validation, password set, session issue) |
|
|
| Onboarding checklist | **Ready** | Server-side progress tracking, step completion, UI wizard |
|
|
| Refresh tokens | **Ready** | Creation, validation, revocation, cleanup method (needs scheduling) |
|
|
| TOTP MFA | **Ready** | Setup, enable, verify, recovery codes, disable |
|
|
| SSO (Google) | **Ready** (needs keys) | Conditional loading, user creation/linking |
|
|
| SSO (Azure AD) | **Ready** (needs keys) | Uses deprecated `passport-azure-ad` (works, consider `@azure/msal-node`) |
|
|
| Passkeys (WebAuthn) | **Ready** | Registration, authentication, removal with lockout protection |
|
|
| Resend activation | **Stubbed** | Always returns success, no actual email sent |
|
|
|
|
---
|
|
|
|
## 13. API Endpoint Reference
|
|
|
|
### Billing (no auth unless noted)
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/billing/create-checkout-session` | No | Create Stripe Checkout, returns `{ url }` |
|
|
| `POST` | `/api/webhooks/stripe` | Stripe sig | Webhook receiver |
|
|
| `GET` | `/api/billing/status?session_id=` | No | Poll provisioning status |
|
|
| `POST` | `/api/billing/portal` | JWT | Stripe Customer Portal (stubbed) |
|
|
|
|
### Auth
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/auth/register` | No | Register new user |
|
|
| `POST` | `/api/auth/login` | No | Login (may return MFA challenge) |
|
|
| `POST` | `/api/auth/refresh` | Cookie | Refresh access token |
|
|
| `POST` | `/api/auth/logout` | Cookie | Logout current session |
|
|
| `POST` | `/api/auth/logout-everywhere` | JWT | Revoke all sessions |
|
|
| `GET` | `/api/auth/activate?token=` | No | Validate activation token |
|
|
| `POST` | `/api/auth/activate` | No | Set password + activate |
|
|
| `POST` | `/api/auth/resend-activation` | No | Resend activation (stubbed) |
|
|
| `GET` | `/api/auth/profile` | JWT | Get user profile |
|
|
| `POST` | `/api/auth/switch-org` | JWT | Switch organization |
|
|
|
|
### MFA
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/auth/mfa/setup` | JWT | Generate QR code + secret |
|
|
| `POST` | `/api/auth/mfa/enable` | JWT | Enable MFA with TOTP code |
|
|
| `POST` | `/api/auth/mfa/verify` | mfaToken | Verify during login |
|
|
| `POST` | `/api/auth/mfa/disable` | JWT | Disable (requires password) |
|
|
| `GET` | `/api/auth/mfa/status` | JWT | Check MFA status |
|
|
|
|
### SSO
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `GET` | `/api/auth/sso/providers` | No | List available providers |
|
|
| `GET` | `/api/auth/google/callback` | OAuth | Google callback handler |
|
|
| `GET` | `/api/auth/azure/callback` | OAuth | Azure callback handler |
|
|
| `DELETE` | `/api/auth/sso/unlink/:provider` | JWT | Unlink SSO account |
|
|
|
|
### Passkeys
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `POST` | `/api/auth/passkeys/register-options` | JWT | Get registration options |
|
|
| `POST` | `/api/auth/passkeys/register` | JWT | Complete registration |
|
|
| `POST` | `/api/auth/passkeys/login-options` | No | Get authentication options |
|
|
| `POST` | `/api/auth/passkeys/login` | No | Authenticate with passkey |
|
|
| `GET` | `/api/auth/passkeys` | JWT | List user's passkeys |
|
|
| `DELETE` | `/api/auth/passkeys/:id` | JWT | Remove a passkey |
|
|
|
|
### Onboarding
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `GET` | `/api/onboarding/progress` | JWT | Get onboarding progress |
|
|
| `PATCH` | `/api/onboarding/progress` | JWT | Mark step complete |
|
|
|
|
---
|
|
|
|
## Database Tables Added (Migration 015)
|
|
|
|
| Table | Purpose |
|
|
|-------|---------|
|
|
| `shared.refresh_tokens` | Stores SHA-256 hashed refresh tokens with expiry/revocation |
|
|
| `shared.stripe_events` | Idempotency ledger for Stripe webhook events |
|
|
| `shared.invite_tokens` | Tracks activation/invite magic links |
|
|
| `shared.onboarding_progress` | Per-org onboarding step completion |
|
|
| `shared.user_passkeys` | WebAuthn credential storage |
|
|
| `shared.email_log` | Stubbed email audit trail |
|
|
|
|
Columns added to existing tables:
|
|
- `shared.organizations`: `stripe_customer_id`, `stripe_subscription_id`, `trial_ends_at`
|
|
- `shared.users`: `totp_verified_at`, `recovery_codes`, `webauthn_challenge`
|