feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.

This commit is contained in:
Rotimi Best
2026-04-12 08:58:41 +01:00
parent 5a8f50f67b
commit d6e812d08b
573 changed files with 31964 additions and 8708 deletions
File diff suppressed because it is too large Load Diff
+130 -47
View File
@@ -3,64 +3,147 @@
## Scope Lock (v1)
1. AI assistant is teacher-only (course team members; not students).
2. Agent executes via API (no local-only execution).
2. Agent executes via API using Vercel AI SDK tool-calling pattern.
3. All actions are org/course-scoped; no cross-org access.
4. Self-hosted: assistant not available; hide UI and block agent routes.
5. Chat lives in a right sidebar (Sheet) from the course header.
6. Supported intents: update lesson text, generate questions from text, draft lesson; exercise tagging TBD (clarify org tags vs template tags).
4. Self-hosted: assistant disabled by default; enabled via `AI_API_KEY` env var.
5. Chat lives in a right sidebar (Sheet) triggered from the course sidebar navigation.
6. Two modes: **Plan Mode** (propose course structure from document or prompt) and **Agent Mode** (execute approved actions).
7. Supported actions: upload PDF/DOCX, generate course plan, create sections, create/update lessons, create exercises with questions.
## Delivery Phases
1. Phase A: Package + types + self-host gate
2. Phase B: Agent API routes + permission checks
3. Phase C: Chat UI + header button
4. Phase D: LLM integration + intent execution
5. Phase E: Polish (quick actions, context, rate limits)
1. Phase 1: Foundation — package + types + DB tables + route skeletons + upload endpoint + sidebar UI shell
2. Phase 2: Agent Core — LLM integration + 9 tools + streaming chat + plan view + agent execution + usage tracking
3. Phase 3: Polish — rate limits, quick actions, self-host gating, credits, stop button
4. Phase 4: More Tools — section/lesson/exercise management (delete, reorder, update metadata)
5. Phase 5: Student Assistant (v2) — student tools, student prompt, course-app surface
## Ticket Breakdown
| ID | Area | Task | Key Files | Dependencies | Done When |
| --- | --- | --- | --- | --- | --- |
| AI-A1 | Package | Create `packages/ai-assistant` with types (Message, Intent, Response) | `packages/ai-assistant/src/types.ts` | None | Package builds; types exportable |
| AI-A2 | Package | Add optional API client for agent chat | `packages/ai-assistant/src/client.ts` | AI-A1 | Client can call agent endpoints (or dashboard uses RPC directly) |
| AI-A3 | Dashboard | Add `isAiAssistantAvailable` util (false when `PUBLIC_IS_SELFHOSTED`) | `apps/dashboard/src/lib/utils/ai-assistant.ts` | None | Util returns bool; hide button when self-hosted |
| AI-B1 | API | Add agent validation schemas (chat payload, intent payloads) | `packages/utils/src/validation/agent/` | None | Zod schemas for chat + intents |
| AI-B2 | API | Add agent routes under `/course/:courseId/agent`; do not register or return 403 when self-hosted | `apps/api/src/routes/course/agent.ts` | AI-B1 | `POST /chat`, `POST /execute` (or combined); blocked when `PUBLIC_IS_SELFHOSTED` |
| AI-B3 | API | Apply auth + courseTeamMember middleware to agent routes | `apps/api/src/routes/course/agent.ts` | AI-B2 | Unauthorized users get 403 |
| AI-B4 | API | Add agent service: parse message → intent → call lesson/exercise services | `apps/api/src/services/agent.ts` | AI-B2 | Service orchestrates intents; returns structured response |
| AI-B5 | API | Verify org/course scoping in every agent mutation | `apps/api/src/services/agent.ts` | AI-B4 | Cross-org requests rejected |
| AI-C1 | Dashboard | Add AI Assistant button to course header | `apps/dashboard/src/lib/features/course/components/course-header.svelte` | AI-A3 | Button visible when `isAiAssistantAvailable` and teacher |
| AI-C2 | Dashboard | Add right Sheet with chat UI (message list + input) | `apps/dashboard/src/lib/features/ai-assistant/ai-assistant-sheet.svelte` | AI-C1 | Sheet opens from button; placeholder chat |
| AI-C3 | Dashboard | Create AI assistant feature module (store, API class) | `apps/dashboard/src/lib/features/ai-assistant/` | AI-A1 | API class calls agent endpoint; store holds messages |
| AI-C4 | Dashboard | Wire chat input → API → message display | `apps/dashboard/src/lib/features/ai-assistant/` | AI-C3, AI-B2 | User can send message; response shows in chat |
| AI-D1 | API | Integrate LLM provider (OpenAI/Anthropic) for chat completion | `apps/api/src/services/agent.ts` | AI-B4 | Agent can generate text from user message |
| AI-D2 | API | Implement intent: update lesson text | Agent service + lessonLanguage service | AI-D1 | "Update this text..." → `updateLessonLanguageService` |
| AI-D3 | API | Implement intent: generate N questions from text | Agent service | AI-D1 | Returns generated questions as text; user copies or we add create flow later |
| AI-D4 | API | Implement intent: draft lesson | Agent service + `createLesson` | AI-D1 | Creates draft lesson; returns lesson ID for navigation |
| AI-D5 | API | Clarify and implement exercise tagging intent | Agent service + tag/course APIs | AI-D1 | Semantic clarified in PRD; implement |
| AI-E1 | Copy | Add i18n keys for AI assistant UI | `apps/dashboard/src/lib/utils/translations/en.json` | AI-C2 | No hardcoded strings |
| AI-E2 | API | Add rate limiting for agent endpoints | Middleware or route-level | AI-B2 | Prevents abuse; configurable limit |
| AI-E3 | Tests | Add auth/role tests for agent routes | API tests | AI-B3 | Unauthorized/student requests return 403 |
### Phase 1: Foundation
## API Contract (Target)
| ID | Area | Task | Key Files | Dependencies |
| --- | --- | --- | --- | --- |
| AI-1.1 | Package | Create `packages/ai-assistant` with types (`AgentRole`, `AgentContext`, `CoursePlan`, `AIProviderConfig`) | `packages/ai-assistant/src/types.ts` | None |
| AI-1.2 | Package | Implement provider factory (`createModel(config)`) with OpenAI + Anthropic | `packages/ai-assistant/src/providers/index.ts` | AI-1.1 |
| AI-1.3 | Package | Implement tool registry (`getToolSchemas(role)`) with 9 v1 tool schemas | `packages/ai-assistant/src/tools/` | AI-1.1 |
| AI-1.4 | Package | Implement system prompt builder with Plan/Agent mode instructions + no-hallucination boundary | `packages/ai-assistant/src/prompt/` | AI-1.1 |
| AI-1.5 | DB | Add `ai_token_usage` and `ai_credit_balance` tables to schema + migration | `packages/db/src/schema.ts` | None |
| AI-1.6 | API | Add agent route skeleton (`/agent/status`, `/agent/chat`, `/agent/upload`) with auth middleware + role detection | `apps/api/src/routes/agent/` | AI-1.1 |
| AI-1.7 | API | Implement `POST /agent/upload` — plan gating (EARLY_ADOPTER+ required, 403 for BASIC), file size validation (max 5MB), PDF parsing (`pdf-parse`) + DOCX parsing (`mammoth`) + Redis storage | `apps/api/src/services/agent/document.ts` | AI-1.6 |
| AI-1.8 | API | Add Redis key generators for agent documents | `apps/api/src/utils/redis/key-generators.ts` | None |
| AI-1.9 | Dashboard | Add "AI Assistant" button to course sidebar nav (gated on `/agent/status`) | `course-sidebar-navigation.svelte` | AI-1.6 |
| AI-1.10 | Dashboard | Build right sidebar Sheet with placeholder chat UI (message list + input + file upload button) | `apps/dashboard/src/lib/features/ai-assistant/` | AI-1.9 |
| AI-1.11 | Dashboard | Implement file upload flow — plan gate (BASIC clicks attachment icon -> `openUpgradeModal()`), client-side 5MB size check, file picker -> upload -> chip display, handle 403 from server by triggering upgrade modal | `apps/dashboard/src/lib/features/ai-assistant/` | AI-1.7, AI-1.10 |
### Agent Chat
### Phase 2: Agent Core
- `POST /course/:courseId/agent/chat`
- Body: `{ message: string, context?: { lessonId?, exerciseId? } }`
- Response: `{ success: true, data: { message: string, intent?: string, actions?: [...] } }`
- Auth: `authMiddleware`, `courseTeamMemberMiddleware`
| ID | Area | Task | Key Files | Dependencies |
| --- | --- | --- | --- | --- |
| AI-2.1 | API | Wire `POST /agent/chat` to `streamText()` with provider factory + tool schemas + system prompt | `apps/api/src/routes/agent/chat.ts` | AI-1.2, AI-1.3, AI-1.4 |
| AI-2.2 | API | Implement tool execute functions: `get_course_structure`, `get_lesson_content`, `get_exercise_details` (read tools) | `apps/api/src/routes/agent/chat.ts` | AI-2.1 |
| AI-2.3 | API | Implement tool execute functions: `create_section`, `create_lesson`, `update_lesson_content` (write tools) | `apps/api/src/routes/agent/chat.ts` | AI-2.1 |
| AI-2.4 | API | Implement tool execute functions: `create_exercise`, `add_questions` (exercise tools) | `apps/api/src/routes/agent/chat.ts` | AI-2.1 |
| AI-2.5 | API | Implement `generate_course_plan` tool — structured `CoursePlan` JSON output | `apps/api/src/routes/agent/chat.ts` | AI-2.1 |
| AI-2.6 | API | Inject uploaded document text from Redis into LLM context when `documentId` present | `apps/api/src/routes/agent/chat.ts` | AI-1.7, AI-2.1 |
| AI-2.7 | API | Token usage tracking — record `promptTokens` + `completionTokens` after each `streamText()` call | `apps/api/src/services/agent/usage.ts` | AI-1.5, AI-2.1 |
| AI-2.8 | API | Token balance check — enforce before each LLM call, return 402 when exhausted | `apps/api/src/services/agent/usage.ts` | AI-2.7 |
| AI-2.9 | Dashboard | Connect chat UI to `/agent/chat` via `@ai-sdk/svelte` `Chat` class | `apps/dashboard/src/lib/features/ai-assistant/` | AI-2.1, AI-1.10 |
| AI-2.10 | Dashboard | Render streamed tokens and tool status cards (spinner -> checkmark) | `apps/dashboard/src/lib/features/ai-assistant/` | AI-2.9 |
| AI-2.11 | Dashboard | Build Plan View component — structured plan card with sections/lessons tree, "Implement Plan" and "Ask for Changes" buttons | `apps/dashboard/src/lib/features/ai-assistant/plan-view.svelte` | AI-2.5, AI-2.9 |
| AI-2.12 | Dashboard | Build Agent Mode progress card — real-time checklist during plan execution | `apps/dashboard/src/lib/features/ai-assistant/progress-card.svelte` | AI-2.11 |
| AI-2.13 | Dashboard | Implement `onFinish` store refresh — course content tree, lesson, exercise data | `apps/dashboard/src/lib/features/ai-assistant/` | AI-2.9 |
| AI-2.14 | Dashboard | Add usage meter to AI assistant sidebar header | `apps/dashboard/src/lib/features/ai-assistant/` | AI-2.8 |
### Optional Structured Execute
### Phase 3: Polish + Safeguards + Credits
- `POST /course/:courseId/agent/execute`
- Body: `{ intent: string, payload: Record<string, unknown> }`
- For tool-calling or explicit intent execution (e.g. from LLM tool use)
- Auth: same as above
| ID | Area | Task | Key Files | Dependencies |
| --- | --- | --- | --- | --- |
| AI-3.1 | API | Tool-level permission re-validation (verify resource belongs to course/org in every tool execute) | `apps/api/src/routes/agent/chat.ts` | AI-2.2, AI-2.3, AI-2.4 |
| AI-3.2 | API | Rate limiting — per-user, per-org requests/minute on agent endpoints | `apps/api/src/middlewares/` | AI-1.6 |
| AI-3.3 | API | Self-host env var detection: hide assistant when no `AI_API_KEY`; uncap rate limits for self-hosted | `apps/api/src/routes/agent/status.ts` | AI-1.6 |
| AI-3.4 | API | Conversation context windowing — trim long message histories, especially after document uploads | `apps/api/src/routes/agent/chat.ts` | AI-2.1 |
| AI-3.5 | API | `POST /agent/credits` — credit purchase endpoint (orgAdmin only) | `apps/api/src/routes/agent/credits.ts` | AI-1.5 |
| AI-3.6 | API | `GET /agent/usage` — detailed usage stats endpoint | `apps/api/src/routes/agent/usage.ts` | AI-2.7 |
| AI-3.7 | Dashboard | Quick-action chips (context-dependent: document upload, draft lesson, generate questions, improve text) | `apps/dashboard/src/lib/features/ai-assistant/` | AI-2.9 |
| AI-3.8 | Dashboard | Stop button during plan execution (graceful halt, keep created content) | `apps/dashboard/src/lib/features/ai-assistant/` | AI-2.12 |
| AI-3.9 | Dashboard | Exhausted-state UI — disable chat input when tokens run out, show upgrade prompt | `apps/dashboard/src/lib/features/ai-assistant/` | AI-2.14 |
| AI-3.10 | Dashboard | Credit purchase UI in org settings | `apps/dashboard/src/lib/features/org-settings/` | AI-3.5 |
## Open Items Before Implementation
## API Contract
1. LLM provider choice (OpenAI vs Anthropic vs configurable).
2. Streaming: v1 non-streaming vs SSE streaming.
3. Exercise tagging semantics (org course tags vs exercise template tags).
4. Draft lesson: create immediately vs return text for paste.
### GET /agent/status
```typescript
// Response
{
enabled: boolean, // false when self-hosted + no AI_API_KEY
role: 'teacher' | 'student',
usage: {
used: number,
allowance: number,
creditBalance: number,
remaining: number
}
}
```
### POST /agent/chat
```typescript
// Request
{
courseId: string,
messages: UIMessage[], // Vercel AI SDK format
context?: {
lessonId?: string,
exerciseId?: string,
documentId?: string
}
}
// Response: SSE stream (toUIMessageStreamResponse())
```
### POST /agent/upload
```typescript
// Request: multipart/form-data with 'file' field (PDF or DOCX, max 5MB)
// Requires: EARLY_ADOPTER+ plan
// Success Response (200)
{
documentId: string,
fileName: string,
mimeType: string,
pageCount: number | null,
wordCount: number,
textPreview: string
}
// Error Responses
// 403: { error: "document_upload_requires_upgrade", upgradeRequired: true } — BASIC plan
// 413: { error: "file_too_large", maxSize: 5242880 } — > 5MB
// 415: { error: "unsupported_file_type", allowed: [...] } — not PDF/DOCX
```
### GET /agent/usage
```typescript
// Response
{
used: number,
allowance: number,
creditBalance: number,
remaining: number,
history: Array<{ date: string, tokens: number }>
}
```
### POST /agent/credits
```typescript
// Request
{ amount: number }
// Response
{ creditBalance: number }
```
File diff suppressed because it is too large Load Diff
+191
View File
@@ -0,0 +1,191 @@
# Org Landing Page Theme Picker + Unified Content Editor
## Context
The org has 3 landing page theme templates (Minimal, Bold, Classic) in `packages/ui/` that all accept `OrgLandingPageProps` uniformly. However:
1. The templates are **only used in Storybook** — the actual public landing page (`landing-page.svelte`) renders its own custom legacy HTML
2. The stored `OrgLandingPageJson` is a complex format (header, aboutUs, faq, contact, mailinglist, etc.) that **doesn't align** with what the templates need
3. There is no theme selection UI
4. There is no edit flow like the course landing page (split-screen with live preview)
This plan: simplifies the stored format to match `OrgLandingPageProps`, replaces the legacy public landing page renderer with the theme components, adds a theme picker in settings, and adds a split-screen content editor modeled after the course landing page editor.
---
## Simplified Data Model
Replace `OrgLandingPageJson` (the DB-stored type) with a structure that maps 1:1 to `OrgLandingPageProps`:
```ts
// apps/dashboard/src/lib/utils/types/org.ts
export interface OrgLandingPageJson {
theme: 'minimal' | 'bold' | 'classic';
hero: {
heading: string;
subheading: string;
primaryAction: { label: string; href: string };
secondaryAction?: { label: string; href: string };
image?: string;
};
navItems: Array<{ label: string; href: string }>;
footerLinks: Array<{ label: string; href: string }>;
footerText: string;
}
```
Sections removed from old format: `header` (with banner/background/show toggles), `aboutUs`, `courses`, `faq`, `contact`, `mailinglist`, `customLinks`, `footer` (social links only).
Fields from org data (not stored in `landingpage`): `orgName` → `org.name`, `logoUrl` → `org.avatarUrl`, `courses` → `orgApi.publicCourses`.
---
## Changes
### 1. Update `OrgLandingPageJson` type
**File:** `apps/dashboard/src/lib/utils/types/org.ts`
Replace the full type with the simplified version above.
### 2. Update settings store with new defaults
**File:** `apps/dashboard/src/lib/features/settings/utils/store.ts`
Replace `landingPageSettings` writable defaults with the new schema:
```ts
{
theme: 'minimal',
hero: {
heading: 'Master the Skills of Tomorrow',
subheading: 'Join thousands of learners and accelerate your career.',
primaryAction: { label: 'Explore Courses', href: '#courses' },
secondaryAction: { label: 'Learn More', href: '#about' },
image: ''
},
navItems: [
{ label: 'Courses', href: '#courses' },
{ label: 'About', href: '#about' }
],
footerLinks: [
{ label: 'Terms', href: '#terms' },
{ label: 'Privacy', href: '#privacy' }
],
footerText: '© 2026 All rights reserved.'
}
```
Also update (or delete and inline) the old store at:
`apps/dashboard/src/lib/features/org/components/settings/landing-page-store.ts`
(currently used by the legacy landing-page.svelte — this store will go away when we replace the renderer).
### 3. Redesign `landingpage.svelte` settings page — theme picker only
**File:** `apps/dashboard/src/lib/features/settings/pages/landingpage.svelte`
Remove all existing form fields. The page now shows only:
- **Theme picker** (`Field.Set` "Choose a Theme"): 3 cards in `grid grid-cols-3 gap-4` using `RadioGroup.Root bind:value={$landingPageSettings.theme}`. Each card:
- Inline SVG thumbnail (distinct per theme — see SVG specs below)
- Theme name label
- Radio selection state: `border-primary ring-2 ring-primary/20` vs `border-border`
- `onValueChange={() => (hasUnsavedChanges = true)}`
- **Save button** already in the route page header — saves only `theme` (plus full JSON as fallback)
- **"Edit Content"** button below the theme picker (`Button variant="outline"`) → navigates to `/org/[slug]/settings/landingpage/edit`
- `UnsavedChanges` component stays
SVG thumbnail specs:
- **Minimal**: white bg, light gray nav bar top, 2 text lines (heading/subheading), 2 course card outlines in a 2-col grid, thin footer line
- **Bold**: dark `#111` bg top-half, large bold heading text block, course cards with image-left layout, dark inverted footer
- **Classic**: dark navy `#1e3a5f` header bar, white body, centered heading, 3-col course card grid, minimal footer
Subscriber fallback for old data (users who previously saved the old format):
```ts
if (!landingpage.theme || !landingpage.hero) {
// Set minimal defaults — discard old incompatible structure
$landingPageSettings = { ...defaultLandingPageSettings };
} else {
$landingPageSettings = landingpage;
}
```
### 4. New split-screen content editor route
**File (new):** `apps/dashboard/src/routes/org/[slug]/settings/landingpage/edit/+page.svelte`
Modeled after `apps/dashboard/src/routes/courses/[id]/landingpage/+page.svelte`.
Layout:
```svelte
<!-- Fixed full-screen overlay, fly transition -->
<div class="fixed inset-0 z-250 h-screen w-screen">
<Sidebar.Provider>
<Sidebar.Root side="left" collapsible="icon">
<OrgLandingPageEditor bind:settings={landingPageSettings} {org} onSave={handleSave} onClose={handleClose} />
</Sidebar.Root>
<Sidebar.Inset class="relative h-screen overflow-y-auto">
<!-- Live theme preview -->
{#if $landingPageSettings.theme === 'bold'}
<BoldLandingPage {...previewProps} />
{:else if $landingPageSettings.theme === 'classic'}
<ClassicLandingPage {...previewProps} />
{:else}
<MinimalLandingPage {...previewProps} />
{/if}
</Sidebar.Inset>
</Sidebar.Provider>
</div>
```
`previewProps` is derived from `$landingPageSettings` + `$currentOrg` + `orgApi.publicCourses` (mapped to `OrgLandingPageProps`).
`handleClose()` → `goto('/org/[slug]/settings/landingpage')`.
### 5. New editor sidebar component
**File (new):** `apps/dashboard/src/lib/features/settings/pages/landingpage-editor.svelte`
Mirrors the section-based structure of the course landing page `editor.svelte`.
Sections (left sidebar menu → click to edit):
1. **Hero** — heading, subheading, primaryAction (label + href), secondaryAction (optional label + href toggle), image (upload widget, used by Bold/Classic)
2. **Navigation** — list of nav items (label + href), add/remove rows
3. **Footer** — footer text, list of footer links (label + href), add/remove rows
Header: "Save" button + close button (back to settings).
Section form pattern: inline `Input` / `Textarea` / `UploadWidget` fields, two-way bound to `settings` prop, changes reflect live in the preview panel.
### 6. Replace legacy public landing page renderer
**File:** `apps/dashboard/src/lib/features/org/components/landing-page/landing-page.svelte`
Replace the full custom HTML renderer with:
- Import `MinimalLandingPage`, `BoldLandingPage`, `ClassicLandingPage` from `@cio/ui/custom/org-landing-page`
- Load `org.landingpage` into local state with defaults fallback
- Build `previewProps: OrgLandingPageProps` = `{ orgName: org.name, logoUrl: org.avatarUrl, ...landingpage, courses: orgApi.publicCourses }`
- Render the correct theme component based on `landingpage.theme`
- Remove all legacy section rendering (aboutUs, faq, contact, mailinglist, etc.)
- Keep `orgApi.getPublicCoursesBySiteName(orgSiteName)` call for course loading
- Keep `<svelte:head>` and `<PoweredBy />`
---
## Critical Files
| File | Change |
|------|--------|
| `apps/dashboard/src/lib/utils/types/org.ts` | Replace `OrgLandingPageJson` type |
| `apps/dashboard/src/lib/features/settings/utils/store.ts` | New defaults matching new type |
| `apps/dashboard/src/lib/features/org/components/settings/landing-page-store.ts` | Delete or update (old format no longer needed) |
| `apps/dashboard/src/lib/features/settings/pages/landingpage.svelte` | Theme picker + Edit Content button only |
| `apps/dashboard/src/routes/org/[slug]/settings/landingpage/+page.svelte` | Minor adjustments if needed |
| `apps/dashboard/src/lib/features/org/components/landing-page/landing-page.svelte` | Replace renderer with theme components |
| `apps/dashboard/src/routes/org/[slug]/settings/landingpage/edit/+page.svelte` | **New** — split-screen editor route |
| `apps/dashboard/src/lib/features/settings/pages/landingpage-editor.svelte` | **New** — editor sidebar component |
## Reused Patterns
- `Sidebar.Provider/Root/Inset` from `@cio/ui/base/sidebar` — same as course landing page editor route
- `RadioGroup.Root/Item` — already imported in `landingpage.svelte`
- `Field.Set/Field.Legend/Field.Separator` — already imported in `landingpage.svelte`
- `UploadWidget` — already imported in `landingpage.svelte`
- `orgApi.update($currentOrg.id, { landingpage: ... })` — same save pattern
- `UnsavedChanges` component — already in `landingpage.svelte`
- `goto()` for close/navigate — same as course editor
## Verification
1. **Theme picker**: `/org/[slug]/settings/landingpage` shows 3 cards with distinct SVG previews; clicking a card highlights it; saving persists the selection; reload reflects saved theme
2. **Edit Content**: clicking "Edit Content" navigates to `/org/[slug]/settings/landingpage/edit`; the editor opens as a full-screen overlay with the selected theme visible on the right; editing fields updates the preview live
3. **Public landing page**: visiting the org's public URL renders the selected theme (Minimal/Bold/Classic) using actual org courses; switching theme in settings and saving changes what public visitors see
4. **Backward compat**: orgs with old `OrgLandingPageJson` format get reset to minimal defaults gracefully (no crash)
@@ -0,0 +1,92 @@
# Org Landing Page Editor Parity Plan
## Scope Lock
1. Keep the existing org landing page feature set, but refactor the editor and preview shell to match the course landing page structure.
2. Preserve the query-parameter handoff for theme preview so content can be edited against a selected theme without publishing it.
3. Do not add section descriptions in the org editor if the course editor does not use them.
4. The editor preview pane must scroll like the course landing page preview pane, not like the current org implementation.
5. The sidebar shell must use the same composition and visual rhythm as the course editor.
6. Keep the editor sections limited to the existing org landing page content areas, including embed support.
## Delivery Phases
1. Phase A: Align the org editor route shell with the course landing page editor shell.
2. Phase B: Split the org editor into small section components.
3. Phase C: Fix scroll behavior, sidebar layout, and header/action placement.
4. Phase D: Clean up remaining copy, state handling, and regression risks.
## Ticket Breakdown
| ID | Area | Task | Key Files | Depends On | Done When |
| --- | --- | --- | --- | --- | --- |
| OLP-1 | Route Shell | Rebuild the org editor route around the same `Sidebar.Provider` / `Sidebar.Root` / `Sidebar.Inset` structure used by the course editor | `apps/dashboard/src/routes/org/[slug]/settings/landingpage/edit/+page.svelte` | None | Full-screen editor behaves like the course editor and preview is rendered in the inset pane |
| OLP-2 | Sidebar Width | Match the course editor sidebar width and spacing defaults | `apps/dashboard/src/routes/org/[slug]/settings/landingpage/edit/+page.svelte` | OLP-1 | Sidebar no longer looks visually broken or oversized |
| OLP-3 | Component Split | Break the monolithic org editor into focused section components | `apps/dashboard/src/lib/features/settings/pages/landingpage-editor.svelte` + new section components | OLP-1 | Editor shell stays thin and section logic is isolated |
| OLP-4 | Hero Section | Extract hero editing into its own form component | `apps/dashboard/src/lib/features/settings/pages/landingpage-editor/*` | OLP-3 | Hero controls render independently and update preview state live |
| OLP-5 | Navigation Section | Extract navigation editing into its own form component | `apps/dashboard/src/lib/features/settings/pages/landingpage-editor/*` | OLP-3 | Nav items can be added, removed, and edited without a long conditional tree |
| OLP-6 | Embed Section | Extract embed editing into its own form component | `apps/dashboard/src/lib/features/settings/pages/landingpage-editor/*` | OLP-3 | Embed fields render cleanly and iframe preview settings remain supported |
| OLP-7 | Footer Section | Extract footer editing into its own form component | `apps/dashboard/src/lib/features/settings/pages/landingpage-editor/*` | OLP-3 | Footer text and links edit cleanly in a dedicated section |
| OLP-8 | Menu Copy | Remove section descriptions from the org editor section list and section header | `apps/dashboard/src/lib/features/settings/pages/landingpage-editor.svelte` | OLP-3 | Sidebar matches the simpler course editor tone and density |
| OLP-9 | Scroll Behavior | Make preview scrolling match the course editor behavior | `apps/dashboard/src/routes/org/[slug]/settings/landingpage/edit/+page.svelte`, org landing page theme components | OLP-1 | The preview pane scrolls independently and the sidebar stays stable |
| OLP-10 | Theme Query Param | Keep theme preview driven by `?theme=` without publishing the theme on content edits | `apps/dashboard/src/lib/features/settings/pages/landingpage.svelte`, `apps/dashboard/src/routes/org/[slug]/settings/landingpage/edit/+page.svelte` | None | Clicking Edit Content previews the selected theme while leaving the live theme unchanged until an explicit settings save |
| OLP-11 | Header Actions | Keep save/close behavior aligned with the course editor style | `apps/dashboard/src/lib/features/settings/pages/landingpage-editor.svelte` | OLP-1 | Save remains explicit and the editor header remains consistent |
| OLP-12 | Regression Check | Verify build and key interaction flows after the refactor | workspace build commands | OLP-1 to OLP-11 | Dashboard build passes and the editor flow is stable |
## Structure Targets
### Route Shell
The org edit route should follow the same outer structure as the course landing page editor:
1. Full-screen fixed overlay.
2. `Sidebar.Provider` as the top-level layout container.
3. `Sidebar.Root` for the left editor panel.
4. `Sidebar.Inset` for the live preview pane.
5. One trigger button in the preview pane, consistent with the course editor.
### Editor Component Shape
The org editor should be a thin orchestration layer that:
1. Tracks selected section state.
2. Renders a compact section list.
3. Delegates form rendering to section-specific components.
4. Handles save and close actions.
5. Binds dirty state to the parent settings page.
### Section Components
Keep the editable sections aligned with the current org landing page model:
1. Hero
2. Navigation
3. Embed
4. Footer
## UX Rules
1. No section descriptions in the org editor if the course editor does not show them.
2. No long `if`/`else` chains in the main editor component when a section registry or split components can express the same behavior.
3. The preview pane should be the only scrollable surface inside the editor shell.
4. Save remains explicit. Theme selection alone should not publish the selected theme when the user is only editing content.
5. The query-param theme preview should be preserved when navigating into and out of the content editor.
## Acceptance Criteria
1. The org landing page editor matches the course landing page editor composition closely enough that the two routes feel like siblings, not separate implementations.
2. The org sidebar no longer appears broken or mis-sized.
3. The preview pane scrolls correctly and does not trap or duplicate scrolling behavior.
4. The org editor sections are split into small, readable components.
5. Section descriptions are removed from the org editor UI.
6. Opening the editor with a selected theme preview does not publish that theme until the user explicitly saves the settings page.
7. `pnpm --filter @cio/dashboard build` passes after the refactor.
## Verification Commands
1. `pnpm --filter @cio/dashboard build`
2. Manually verify `/org/[slug]/settings/landingpage`
3. Manually verify `/org/[slug]/settings/landingpage/edit?theme=minimal`
4. Manually verify `/org/[slug]/settings/landingpage/edit?theme=bold`
5. Manually verify `/org/[slug]/settings/landingpage/edit?theme=classic`
+747 -473
View File
File diff suppressed because it is too large Load Diff
+290 -276
View File
@@ -1,365 +1,379 @@
---
name: Public API with Keys
overview: Build a versioned public API (v1) authenticated via shared organization-scoped automation keys reused by MCP and Public API, with course CRUD, course export, student/audience CRUD endpoints, plus a dashboard Automation page with MCP, Zapier, and API tabs.
name: Public API with Organization-Scoped Automation Keys
overview: Build the public API on top of the shipped organization automation foundation: the `/org/[slug]/automation` section already exists, MCP is live, `/automation/api` and `/automation/zapier` exist as placeholder pages, and org-scoped automation keys already exist in the backend. This PRD updates the remaining work to ship the actual public API experience and docs.
todos:
- id: db-schema
content: Add `api_key` table to schema.ts, update relations, generate migration
- id: confirm-existing-foundation
content: Confirm and preserve the shipped automation foundation (`Automation` nav, `/org/[slug]/automation` redirect, MCP page, organization automation key backend)
status: completed
- id: org-automation-foundation
content: Keep using `organization_api_key` and `/organization/automation/*` as the shared automation-key foundation for MCP, API, and Zapier
status: completed
- id: mcp-page-reference
content: Use the existing MCP implementation as the structural reference for the API page (route shell, server load, feature API class, feature page component, translations)
status: completed
- id: agents-guidance
content: Ensure all implementation in this initiative follows `AGENTS.md`
status: completed
- id: api-tab-page
content: Replace the placeholder `/org/[slug]/automation/api` page with a real API page that follows the MCP page pattern
status: pending
- id: db-queries
content: Create api-key query functions (create, getByHash, getByOrg, revoke, updateLastUsed)
- id: api-tab-server-load
content: Add `/org/[slug]/automation/api/+page.server.ts` to load API key and usage data using the existing organization automation endpoints
status: pending
- id: api-key-middleware
content: Build apiKeyMiddleware that authenticates via Bearer token and sets orgId in context
- id: api-tab-copy
content: Add API-specific translations and content for docs links, usage messaging, and key management states
status: pending
- id: validation-schemas
content: Add Zod schemas for shared automation key management and public API input validation
- id: v1-validation-schemas
content: Add public API input/output validation schemas on top of the existing automation-key schemas
status: pending
- id: api-key-service
content: Create api-key service (key generation, hashing, CRUD)
status: pending
- id: api-key-routes
content: Create session-authenticated automation key management routes reused by MCP and Public API
status: pending
- id: v1-course-service
content: Create v1 course service (list, get, create, update, delete, export, students)
status: pending
- id: v1-audience-service
content: Create v1 audience service (list, get, add, update, remove)
- id: v1-services
content: Create versioned public API services for courses and audience/students
status: pending
- id: v1-routes
content: Create versioned v1 router with course and audience sub-routers, register in app.ts
content: Create and register `/v1` public API routes authenticated by organization automation keys
status: pending
- id: public-api-scopes
content: Finalize the public API scope model and middleware requirements for `api` and `zapier` automation keys
status: pending
- id: tests
content: Write tests for api-key, v1/courses, v1/audience endpoints using Hono app.request()
status: pending
- id: translations
content: Add automation-related translation keys to en.json
status: pending
- id: sidebar-nav
content: Add Automation nav item to org-navigation.ts
status: pending
- id: automation-page
content: Create automation page with Tabs (MCP/Zapier/API), admin-only controls, and key setup flows
status: pending
- id: automation-api-class
content: Create frontend API class for api-key management
status: pending
- id: build-verify
content: Build API package, verify no lint errors, run tests
content: Add API tests for automation auth, `/v1` routes, and contract coverage
status: pending
- id: openapi-contracts
content: Ensure all new public API routes are included in OpenAPI generation (route metadata + schemas)
content: Ensure all public API routes are represented in generated OpenAPI output
status: pending
- id: openapi-publish
content: Regenerate and upload latest OpenAPI spec via `upload-openapi-spec.ts`
content: Regenerate and upload the latest OpenAPI spec via `upload-openapi-spec.ts`
status: pending
- id: docs-openapi-integration
content: Integrate OpenAPI docs page in `apps/docs` using Fumadocs OpenAPI integration
content: Add the public API docs experience in `apps/docs`
status: pending
- id: docs-openapi-nav
content: Add `API` docs navigation entry/page in `apps/docs/content/docs/meta.json`
- id: build-verify
content: Build and verify API and docs packages after implementation
status: pending
isProject: false
---
# Public API with Organization-Scoped API Keys
# Public API with Organization-Scoped Automation Keys
## Architecture Overview
## Goal
```mermaid
flowchart TB
subgraph client [External Client]
APIKey["API Key in Authorization header"]
end
subgraph api [API Server]
V1Router["/v1 Router"]
APIKeyMiddleware["apiKeyMiddleware"]
CourseRoutes["Course Routes"]
AudienceRoutes["Audience Routes"]
APIKeyRoutes["API Key Management"]
end
subgraph db [Database]
APIKeyTable["api_key table"]
OrgTable["organization"]
CourseTable["course / group"]
AudienceTable["groupmember / profile"]
end
Ship a versioned public API on top of the existing organization automation platform. The current codebase already supports shared org-scoped automation keys and a dedicated Automation section in `/org`; this PRD now focuses on the remaining API product work rather than re-specifying the already-shipped foundation.
client -->|"Bearer cio_..."| V1Router
V1Router --> APIKeyMiddleware
APIKeyMiddleware -->|"validates + resolves orgId"| APIKeyTable
APIKeyMiddleware --> CourseRoutes
APIKeyMiddleware --> AudienceRoutes
APIKeyRoutes -->|"session auth"| APIKeyTable
```
## Implementation Guardrails
- All implementation in this initiative must follow [AGENTS.md](../../AGENTS.md).
- Do not design this as a brand-new standalone automation surface. Follow the existing patterns already used by the MCP implementation.
- For dashboard work, the API page must mirror the MCP structure:
- route shell under `apps/dashboard/src/routes/org/[slug]/automation/*`
- server-side data loading in `+page.server.ts`
- state and mutations in `apps/dashboard/src/lib/features/automation/api/automation.svelte.ts`
- request/response types in `apps/dashboard/src/lib/features/automation/utils/types.ts`
- page composition in `apps/dashboard/src/lib/features/automation/pages/api.svelte`
- Do not define new dashboard types inside `.svelte.ts` files. Keep feature types in `utils/types.ts` and infer them from the API client.
- Keep user-facing copy in `apps/dashboard/src/lib/utils/translations/en.json`.
## Current State Confirmed in Code
## 1. Database: `api_key` Table
### Org navigation and routes
Add a new table in [packages/db/src/schema.ts](packages/db/src/schema.ts):
These pieces already exist and should be treated as the current baseline:
```typescript
export const apiKey = pgTable('api_key', {
id: uuid('id').defaultRandom().primaryKey(),
organizationId: uuid('organization_id').notNull().references(() => organization.id, { onDelete: 'cascade' }),
keyHash: text('key_hash').notNull(), // SHA-256 hash of the key
keyPrefix: varchar('key_prefix', { length: 12 }).notNull(), // "cio_live_xxxx" for display
name: varchar('name', { length: 255 }).notNull().default('Default'),
lastUsedAt: timestamp('last_used_at'),
expiresAt: timestamp('expires_at'),
isRevoked: boolean('is_revoked').notNull().default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
createdBy: uuid('created_by').notNull().references(() => profile.id),
});
```
- `apps/dashboard/src/lib/features/ui/navigation/org-navigation.ts`
- contains an admin-only `Automation` section under `/org/[slug]/automation`
- exposes nested entries for `MCP`, `Zapier`, and `API`
- `apps/dashboard/src/routes/org/[slug]/automation/+page.ts`
- redirects `/org/[slug]/automation` to `/org/[slug]/automation/mcp`
- `apps/dashboard/src/routes/org/[slug]/automation/mcp/+page.svelte`
- shipped MCP route shell using `Page.Root`, `Page.Header`, and `Page.Body`
- `apps/dashboard/src/routes/org/[slug]/automation/mcp/+page.server.ts`
- already loads automation keys and usage for `type=mcp`
- `apps/dashboard/src/routes/org/[slug]/automation/api/+page.svelte`
- already exists, but is currently a placeholder
- `apps/dashboard/src/routes/org/[slug]/automation/zapier/+page.svelte`
- already exists, but is currently a placeholder
- The raw key is only shown once on creation; we store a SHA-256 hash.
- `keyPrefix` (e.g. `cio_live_a1b2`) lets users identify keys in the UI without exposing the secret.
- DON'T GENERATE MIGRATION, when we run `pnpm db push` it will automatically apply the changes.
### Frontend automation feature foundation
## 2. API Key Queries
These pieces already exist and should be reused:
New file: [packages/db/src/queries/api-key/api-key.ts](packages/db/src/queries/api-key/api-key.ts)
- `apps/dashboard/src/lib/features/automation/api/automation.svelte.ts`
- shared automation API class for list/create/revoke/rotate
- `apps/dashboard/src/lib/features/automation/utils/types.ts`
- shared request and response inference types
- `apps/dashboard/src/lib/features/automation/pages/mcp.svelte`
- the current reference implementation for page structure and composition
- `apps/dashboard/src/lib/features/automation/pages/api.svelte`
- currently renders only a coming-soon callout
- `apps/dashboard/src/lib/utils/translations/en.json`
- already contains automation navigation and MCP copy
Functions:
### Backend automation-key foundation
- `createApiKey(orgId, createdBy, name?)` -- inserts row, returns id
- `getApiKeyByHash(hash)` -- lookup for auth middleware
- `getApiKeysByOrganization(orgId)` -- list keys (prefix, name, createdAt, lastUsedAt, isRevoked)
- `revokeApiKey(keyId, orgId)` -- sets `isRevoked = true`
- `updateApiKeyLastUsed(keyId)` -- stamps `lastUsedAt`
This work is already implemented and should not be re-invented as a separate `/api-key` system:
## 3. API Key Auth Middleware
- `packages/db/src/schema.ts`
- already defines `organization_api_key`
- already defines `organization_automation_usage`
- `packages/utils/src/validation/organization/automation-key.ts`
- already defines key types: `mcp`, `api`, `zapier`
- already defines scopes, query params, creation payload, and param validation
- `packages/db/src/queries/organization/automation-key.ts`
- already handles persistence for organization automation keys
- `apps/api/src/services/organization/automation-key.ts`
- already handles secret generation, hashing, creation, revoke, rotate, and auth
- `apps/api/src/routes/organization/automation.ts`
- already exposes:
- `GET /organization/automation/usage`
- `GET /organization/automation/keys`
- `POST /organization/automation/keys`
- `DELETE /organization/automation/keys/:keyId`
- `POST /organization/automation/keys/:keyId/rotate`
New file: [apps/api/src/middlewares/api-key.ts](apps/api/src/middlewares/api-key.ts)
### Important correction to the old PRD
- Reads `Authorization: Bearer cio_live_...` header
- SHA-256 hashes the key, looks up via `getApiKeyByHash`
- Checks `isRevoked`, `expiresAt`
- Sets `c.set('orgId', key.organizationId)` on context (same pattern as `orgMemberMiddleware`)
- Returns 401 on invalid/expired/revoked key
The old PRD assumed:
## 4. Validation Schemas
- a brand-new Automation page still needed to be created
- Automation should be one page with internal tabs
- a standalone `/api-key` backend should be introduced
- `Webhooks` was one of the primary tabs
New file: [packages/utils/src/validation/api-key/api-key.ts](packages/utils/src/validation/api-key/api-key.ts)
That is no longer accurate.
Schemas for API key management and public API input validation:
The current product structure is:
- `ZCreateApiKey` -- `{ name?: string }`
- `ZRevokeApiKeyParam` -- `{ keyId: string (uuid) }`
- Public API schemas for course CRUD, audience CRUD, pagination, etc.
- org navigation owns the Automation section
- Automation is split into nested routes: `mcp`, `zapier`, and `api`
- the shared backend lives under `/organization/automation/*`
- MCP is the shipped reference page
- `api` and `zapier` are present as placeholder pages
## 5. Public API Routes (Versioned)
## Existing Foundation to Build On
### 1. Shared Automation Keys
The public API must reuse the existing organization-scoped automation-key model.
Current model:
- table: `organization_api_key`
- key types: `mcp`, `api`, `zapier`
- key prefixes: generated from the actual secret prefix
- auth: bearer token via the existing automation-key service/middleware path
- rotation and revocation: already implemented
- usage tracking: already implemented in `organization_automation_usage`
Design requirement:
- do not introduce a second key table or a second dashboard key-management surface
- do not reintroduce the older `/api-key` route design from the original PRD
- build the public API on the existing `organization/automation` contracts unless there is a deliberate migration plan
### 2. Dashboard Information Architecture
The Automation product is already organized as nested org routes, not a tabbed single-page shell.
Required route structure:
- `/org/[slug]/automation`
- redirect only
- `/org/[slug]/automation/mcp`
- existing reference implementation
- `/org/[slug]/automation/api`
- target page for public API setup and management
- `/org/[slug]/automation/zapier`
- separate placeholder or future implementation
Design requirement:
- keep the org sidebar/navigation pattern unchanged
- do not replace nested routes with a new internal tabs container for top-level Automation navigation
- if tabs are used inside a page, use them only for local subcontent, the way MCP uses setup tabs for client snippets
## Remaining Product Scope
### 3. API Page in Dashboard
The next dashboard milestone is to replace the placeholder API page with a real implementation.
### Target route files
- `apps/dashboard/src/routes/org/[slug]/automation/api/+page.svelte`
- `apps/dashboard/src/routes/org/[slug]/automation/api/+page.server.ts`
- `apps/dashboard/src/lib/features/automation/pages/api.svelte`
- `apps/dashboard/src/lib/features/automation/api/automation.svelte.ts`
- `apps/dashboard/src/lib/features/automation/utils/types.ts`
- `apps/dashboard/src/lib/utils/translations/en.json`
### Required structure
The API page must follow the same structural pattern already used by MCP:
1. Route shell:
- use `Page.Root`, `Page.Header`, `Page.HeaderContent`, `Page.Title`, and `Page.Body`
- set the page title from `automation.tabs.api`
- use an API-specific subtitle, not the generic coming-soon copy
2. Server load:
- add `+page.server.ts`
- load key data with `classroomio.organization.automation.keys.$get({ query: { type: 'api' } })`
- load usage data with `classroomio.organization.automation.usage.$get({ query: { type: 'api' } })`
- hydrate the shared `automationApi` store the same way MCP currently does
3. Feature page:
- keep composition in `apps/dashboard/src/lib/features/automation/pages/api.svelte`
- mirror the MCP page layout style and sectioning
- do not leave this page as a single callout
### Required API page sections
The real API page should include the following, using the MCP page as the UX baseline:
- admin permission callout for non-admin users
- usage cards or equivalent summary block for API-specific limits
- API key table filtered to `type === 'api'`
- create key flow
- rotate key flow
- revoke key flow
- one-time secret reveal after creation or rotation
- docs/discovery section with a link to the public API documentation
- API-specific setup guidance
### Reuse requirements
- reuse `automationApi` instead of introducing a separate API-only dashboard client
- reuse shared automation types in `utils/types.ts`
- add new helper utilities only if the API page needs API-specific setup snippets or docs helpers
### 4. Public API Routes
The actual public API is still pending.
### Router structure
New directory: `apps/api/src/routes/v1/`
Create:
Files:
- `apps/api/src/routes/v1/index.ts`
- `apps/api/src/routes/v1/courses.ts`
- `apps/api/src/routes/v1/audience.ts`
- [apps/api/src/routes/v1/courses.ts](apps/api/src/routes/v1/courses.ts) -- Course endpoints
- [apps/api/src/routes/v1/audience.ts](apps/api/src/routes/v1/audience.ts) -- Audience endpoints
- [apps/api/src/routes/v1/index.ts](apps/api/src/routes/v1/index.ts) -- Combines sub-routers
Register in [apps/api/src/app.ts](apps/api/src/app.ts):
Register in `apps/api/src/app.ts` as:
```typescript
.route('/v1', v1Router)
.route('/public-api/v1', v1Router)
```
All v1 routes use `apiKeyMiddleware` (not session auth).
### Authentication model
### Course Endpoints (`/v1/courses`)
All `/public-api/v1` routes must authenticate with organization automation keys.
Requirements:
| Method | Path | Description |
| ------ | -------------------------------- | -------------------------------------------------- |
| GET | `/v1/courses` | List all org courses (paginated) |
| POST | `/v1/courses` | Create a course |
| GET | `/v1/courses/:courseId` | Get course details |
| PUT | `/v1/courses/:courseId` | Update a course |
| DELETE | `/v1/courses/:courseId` | Soft-delete a course |
| GET | `/v1/courses/:courseId/export` | Export course content (lessons, exercises) as JSON |
| GET | `/v1/courses/:courseId/students` | List students enrolled in a course |
- use the existing automation-key authentication path
- require `type: 'api'` or an equivalent scope model that intentionally permits public API access
- document and enforce scopes clearly
- do not rely on session auth for `/public-api/v1`
### Planned endpoint groups
### Audience Endpoints (`/v1/audience`)
Initial public API surface:
| Method | Path | Description |
| ------ | ---- | ----------- |
| GET | `/public-api/v1/courses` | List organization courses |
| POST | `/public-api/v1/courses` | Create a course |
| GET | `/public-api/v1/courses/:courseId` | Get course details |
| PUT | `/public-api/v1/courses/:courseId` | Update a course |
| DELETE | `/public-api/v1/courses/:courseId` | Soft-delete a course |
| GET | `/public-api/v1/courses/:courseId/export` | Export course content as JSON |
| GET | `/public-api/v1/courses/:courseId/students` | List enrolled students |
| GET | `/public-api/v1/audience` | List organization audience members |
| POST | `/public-api/v1/audience` | Add a student to the organization |
| POST | `/public-api/v1/audience/assign-courses` | Assign one or more audience members to one or more courses |
| GET | `/public-api/v1/audience/:studentId` | Get student details |
| PUT | `/public-api/v1/audience/:studentId` | Update student details |
| DELETE | `/public-api/v1/audience/:studentId` | Remove a student from the organization |
| Method | Path | Description |
| ------ | ------------------------- | --------------------------------- |
| GET | `/v1/audience` | List all org students (paginated) |
| POST | `/v1/audience` | Add a student to the org |
| GET | `/v1/audience/:studentId` | Get student details |
| PUT | `/v1/audience/:studentId` | Update student info |
| DELETE | `/v1/audience/:studentId` | Remove student from org |
Enrollment decision:
- keep only the audience assignment endpoint for enrollment
- do not add a separate course-centric enrollment route such as `POST /public-api/v1/courses/:courseId/students`
- a single audience member plus a single course in `POST /public-api/v1/audience/assign-courses` covers the single-enrollment case
- multiple audience members and multiple courses in the same payload cover the bulk-assignment case
### API Key Management (session-authenticated, not public API)
### 5. Validation and Services
New file: [apps/api/src/routes/api-key/api-key.ts](apps/api/src/routes/api-key/api-key.ts)
Add public API schemas and services on top of the existing automation-key foundation.
Target files:
| Method | Path | Description |
| ------ | ----------------- | ------------------------------------------- |
| POST | `/api-key` | Generate new API key (returns raw key once) |
| GET | `/api-key` | List API keys for the org |
| DELETE | `/api-key/:keyId` | Revoke an API key |
- `packages/utils/src/validation/...`
- `apps/api/src/services/v1/course.ts`
- `apps/api/src/services/v1/audience.ts`
Requirements:
Registered in `app.ts` as `.route('/api-key', apiKeyRouter)`. Protected by `authMiddleware` + `orgAdminMiddleware`.
- follow the route/service/validation split defined in `AGENTS.md`
- each route must return a single response type shape
- keep business logic in services, not routes
- reuse existing DB queries where possible
## 6. Services
### 6. OpenAPI and Docs
- [apps/api/src/services/api-key.ts](apps/api/src/services/api-key.ts) -- Key generation (crypto.randomBytes), hashing, CRUD logic
- [apps/api/src/services/v1/course.ts](apps/api/src/services/v1/course.ts) -- Public API course business logic (reuses existing queries from `@cio/db/queries/course`)
- [apps/api/src/services/v1/audience.ts](apps/api/src/services/v1/audience.ts) -- Public API audience business logic (reuses `getOrganizationAudience` and related queries)
The backend already has OpenAPI infrastructure. The remaining work is to ensure the new public API is included and discoverable.
## 7. Tests
### OpenAPI requirements
New test files under `apps/api/src/__tests__/`:
- all new `/public-api/v1/*` routes must be documented with the existing Hono OpenAPI patterns
- request params, query, body, success, and error responses must be declared
- avoid undocumented public API routes
- `api-key.test.ts` -- API key creation, listing, revocation, auth flow
- `v1/courses.test.ts` -- All course CRUD + export + students endpoints
- `v1/audience.test.ts` -- All audience CRUD endpoints
### Spec publishing
Tests will use Hono's `app.request()` test helper (no HTTP server needed). Mock the DB layer with `vi.mock()`.
Use the existing script:
Additionally, include OpenAPI contract coverage:
- verify all `/v1/*` and `/api-key/*` public-facing endpoints are represented in generated spec.
- ensure schema components are generated for request/response payloads.
- `apps/api/scripts/upload-openapi-spec.ts`
## 8. Dashboard UI: Automation Page
Required flow after API changes:
### Sidebar Navigation
1. Implement or update routes and schemas.
2. Run `pnpm --filter @cio/api upload:openapi`.
3. Verify the latest spec includes the new routes.
Add an "Automation" nav item in [apps/dashboard/src/lib/features/ui/navigation/org-navigation.ts](apps/dashboard/src/lib/features/ui/navigation/org-navigation.ts), placed before Settings, with `requiresAdmin: true`:
### Docs integration
```typescript
{
titleKey: 'org_navigation.automation',
path: '/automation',
icon: AutomationIcon, // use Zap or Workflow icon from lucide
requiresAdmin: true,
matchPattern: '^/org/[^/]+/automation(/.*)?$'
}
```
Add the public API docs experience in `apps/docs`.
### Route
Target tasks:
New file: [apps/dashboard/src/routes/org/[slug]/automation/+page.svelte](apps/dashboard/src/routes/org/[slug]/automation/+page.svelte)
1. Add the API docs page or route.
2. Wire it to the published OpenAPI source.
3. Add docs navigation for API.
4. Verify the docs build still passes.
### Page Structure
### 7. Testing
```
+----------------------------------------------+
| Automation |
| Connect ClassroomIO with your tools and |
| workflows. |
+----------------------------------------------+
| [ API ] [ Zapier ] [ Webhooks ] |
+----------------------------------------------+
| |
| (Tab content based on selection) |
| |
+----------------------------------------------+
```
Add tests for:
- Title: "Automation" with description text
- Uses `Tabs` from `@cio/ui/base/tabs`
- Three tabs: **API**, **Zapier**, **Webhooks**
- organization automation key auth behavior relevant to public API access
- `/v1/courses`
- `/v1/audience`
- OpenAPI contract inclusion for public API routes
### API Tab Content
Use Hono app-level testing where possible.
**Free plan users:** Show `UpgradeBanner` component (same pattern as other pages). Disable all controls.
### 8. Build and Verification
**Paid plan users:**
Required verification commands after implementation:
- API key display field (masked by default, e.g. `cio_live_••••••••••••a1b2`)
- "Show" / "Hide" toggle button to reveal/mask the key
- "Copy" button to copy to clipboard
- "Regenerate" button (with confirmation) to create a new key and revoke the old one
- "Read the Docs" button/link pointing to the API documentation URL
### API Class
New file: [apps/dashboard/src/lib/features/automation/api/automation.svelte.ts](apps/dashboard/src/lib/features/automation/api/automation.svelte.ts)
Extends `BaseApi`, methods:
- `fetchApiKeys()` -- GET `/api-key`
- `createApiKey()` -- POST `/api-key`
- `revokeApiKey(keyId)` -- DELETE `/api-key/:keyId`
### Frontend Types
New file: [apps/dashboard/src/lib/features/automation/utils/types.ts](apps/dashboard/src/lib/features/automation/utils/types.ts)
### Translations
Add keys to [apps/dashboard/src/lib/utils/translations/en.json](apps/dashboard/src/lib/utils/translations/en.json):
- `org_navigation.automation`
- `automation.title`, `automation.description`
- `automation.tabs.api`, `automation.tabs.zapier`, `automation.tabs.webhooks`
- `automation.api.key_label`, `automation.api.copy`, `automation.api.regenerate`, `automation.api.read_docs`
- `automation.api.upgrade_prompt`
- `automation.zapier.coming_soon`, `automation.webhooks.coming_soon`
## 9. Versioning Strategy
- All public API routes live under `/v1/`. When v2 is needed, a new `apps/api/src/routes/v2/` directory is created with its own router, registered as `.route('/v2', v2Router)`.
- The v1 router remains untouched, ensuring backward compatibility.
- The API key middleware is version-agnostic and shared across all versions.
## 10. OpenAPI Spec Requirements
### Route Annotation Requirements
All new public API routes in this PRD must be OpenAPI-documented:
- add route metadata using existing Hono OpenAPI patterns (`describeRoute`, validators/schemas).
- ensure request params/query/body and success/error response shapes are declared.
- avoid undocumented routes under `/v1/*` and `/api-key/*`.
### Spec Generation and Publishing
Use existing script in `apps/api/scripts/upload-openapi-spec.ts`:
- generate spec from app routes.
- save local output to `apps/api/dist/openapi/openapi.json`.
- upload dated + latest specs to R2 (`openapi/openapi-YYYY-MM-DD.json`, `openapi/openapi-latest.json`).
Required update flow after public API changes:
1. implement/adjust routes and schemas.
2. run `pnpm --filter @cio/api upload:openapi`.
3. verify `openapi-latest.json` contains new endpoints.
### CI/Verification Gate
Add a verification step in this initiative to prevent stale API docs:
- PR checklist requires successful OpenAPI generation for API changes.
- include generated-spec validation in API CI for public API route changes.
## 11. Docs Integration (Fumadocs OpenAPI)
Integrate API docs in `apps/docs` following Fumadocs OpenAPI integration guidance:
- reference: `https://www.fumadocs.dev/docs/integrations/openapi`
- wire docs app to render OpenAPI spec from latest published source.
Target implementation tasks:
1. Add required Fumadocs OpenAPI integration package(s) to `apps/docs/package.json`.
2. Add a docs route/page named `API` at `/api` in `apps/docs/src/routes` (or equivalent docs content route).
3. Configure the page to load `openapi-latest.json` URL (env-driven preferred, with production fallback).
4. Add an `API` entry to docs navigation in `apps/docs/content/docs/meta.json`.
5. Validate local docs build still passes with the OpenAPI page.
Suggested file touchpoints:
- `apps/docs/package.json`
- `apps/docs/src/routes/*` (new `/api` route) or `apps/docs/content/docs/api.mdx`
- `apps/docs/content/docs/meta.json`
- optional docs config/util files for OpenAPI URL env handling
## 12. Build and Verification Commands
1. `pnpm --filter @cio/api build`
2. `pnpm --filter @cio/api upload:openapi`
3. `pnpm --filter @cio/docs build`
4. Manual check: docs navigation shows `API`, open `/api`, and verify new endpoints appear.
Note: Follow AGENTS.md for code practices.
Manual verification:
1. `/org/[slug]/automation` still redirects to `/org/[slug]/automation/mcp`
2. `/org/[slug]/automation/api` now renders the real API page rather than the placeholder
3. API key creation, rotation, and revoke flows work for `type=api`
4. `/public-api/v1/*` routes respond under the new base path
5. docs navigation exposes the public API docs entry