diff --git a/AGENTS.md b/AGENTS.md index 25b1610..4006341 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,7 @@ Requires `.env` with at least `DATABASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH - **Better-Auth** server instance in `src/lib/auth.ts`, browser client in `src/lib/auth-client.ts`. Session fetched in `src/hooks.server.ts`. - **Roles**: integer hierarchy (SUPER=1, ADMIN=2, STAFF=3, USER=4). `canAccessAdmin` checks roleId <= 3. - **Payments are stubbed** (`src/lib/payments.ts` — setTimeout + console.log). +- **API keys**: SHA-256 hashed before storage (`ApiKey` model); only prefix (11 chars) stored for identification. `/api/v1/*` requires `Authorization: Bearer `, except `/api/v1/automations/` (GHL webhook, no API key needed). Rotation: normal (48h grace via `expiresAt`) or emergency (immediate via `revokedAt`). Super-only admin page at `/admin/api-keys`. ## Formatting conventions @@ -291,3 +292,72 @@ The `$effect` resets `dirty = false` on mount (when the record prop changes). - `.svelte-kit/` — SvelteKit generated types (gitignored) - `src/lib/generated/prisma/` — Prisma client output (not gitignored; regenerate with `npx prisma generate`) + +--- + +## API v1 Endpoints + +| Route | Method | Auth | Description | +| ------------------------------------ | ------ | -------- | ------------------------------------------------ | +| `/api/v1/categories` | GET | API key | List all proclamation categories | +| `/api/v1/frequencies` | GET | API key | List all solfeggio frequencies | +| `/api/v1/proclamations` | GET | API key | List proclamations (with optional `?key` + `?email` filter) | +| `/api/v1/license` | POST | API key | Validate key: `{ license_key, email_address }` | +| `/api/v1/automations/create/account` | POST | None | GHL webhook: create user + license key + emails | + +## GHL Webhook — Auto Account Creation + +`POST /api/v1/automations/create/account` — excluded from API key auth in `hooks.server.ts`. + +**Payload:** + +```json +{ "email": "...", "name": "...", "subscription_level": "Basic" } +``` + +`subscription_level` must match a `SubscriptionLevel.name` in the database. + +**Flow:** +1. If user email doesn't exist → `auth.api.signUpEmail()` with random temp password +2. Query `Verification` table for the auto-generated verification token → construct verify URL +3. Send `automated-user-registration` Resend template (variables: `firstName`, `tempPassword`, `verifyUrl`, `loginUrl`) +4. Create `LicenseKey` record (`active: true`, `level: subscription_level`) +5. Send `license-key-delivery` Resend template (variables: `firstName`, `license_key`) +6. If user already exists → skip signup, just create key + send delivery email + +**Better Auth email callbacks** (`sendVerificationEmail`, `sendResetPassword`) use `void` + `.catch()` — template send failures are logged, never crash the server. + +## Resend Email Templates + +| Template ID | Variables | Subject (API-level) | +| ------------------------------ | --------------------------------------------------------------- | ------------------------------ | +| `automated-user-registration` | `firstName`, `tempPassword`, `verifyUrl`, `loginUrl` | "Your account is ready" | +| `license-key-delivery` | `firstName`, `license_key` | "Your Rhemawave License Key" | +| `email-verification-email` | `target_email_address`, `verification_link` | "Verify your email address" | + +Variables must be declared in the Resend template editor's Variables section. Undeclared variables render as literal `{{{varName}}}` text. + +Controlled by `src/lib/server/resend.ts` (`sendMail`) and `src/lib/server/email-verification.ts`. + +## AdminTable Drag-and-Drop + +AdminTable supports full-`` drag-and-drop reorder via props: + +- `draggable: boolean` — enables drag handles on rows +- `onReorder: (ids: string[]) => Promise` — called after drop with new row ID order +- `copyableFields: string[]` — field keys to show a copy-to-clipboard icon + +**Pattern:** `$derived` + `dragOverride` — no `$effect` sync. `handleDragOver` always calls `preventDefault()` for reliable drop targets. `handleDrop` awaits `onReorder` then calls `invalidateAll()`. + +See `src/lib/components/admin/AdminTable.svelte` and `src/routes/(protected)/admin/proclamation-categories/+page.svelte` for example usage. + +## License Key Admin + +- **Create**: `/admin/license-keys` form → `?/createKey` action. Requires email + subscription level. Generates key, persists to DB, emails the key via Resend template. +- **Replace**: Table row action → `?/replace` action. Deletes old key, creates new one with same user + level. No email sent. +- **Subscription level** stored as `SubscriptionLevel.name` string (not a FK). + +## Key Generation + +- **License keys**: 16 uppercase hex chars in `XXXX-XXXX-XXXX-XXXX` format. `crypto.randomUUID()` → strip dashes → uppercase → 4x4 chunks. Level stored as `SubscriptionLevel.name` string (not FK). +- **API keys**: `sk_` + 64 random hex chars. SHA-256 hashed for storage; first 11 chars stored as `keyPrefix` for identification.