--- 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. todos: - id: db-schema content: Add `api_key` table to schema.ts, update relations, generate migration status: pending - id: db-queries content: Create api-key query functions (create, getByHash, getByOrg, revoke, updateLastUsed) status: pending - id: api-key-middleware content: Build apiKeyMiddleware that authenticates via Bearer token and sets orgId in context status: pending - id: validation-schemas content: Add Zod schemas for shared automation key management and public API input validation 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) status: pending - id: v1-routes content: Create versioned v1 router with course and audience sub-routers, register in app.ts 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 status: pending - id: openapi-contracts content: Ensure all new public API routes are included in OpenAPI generation (route metadata + schemas) status: pending - id: openapi-publish content: Regenerate and upload 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 status: pending - id: docs-openapi-nav content: Add `API` docs navigation entry/page in `apps/docs/content/docs/meta.json` status: pending isProject: false --- # Public API with Organization-Scoped API Keys ## Architecture Overview ```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 client -->|"Bearer cio_..."| V1Router V1Router --> APIKeyMiddleware APIKeyMiddleware -->|"validates + resolves orgId"| APIKeyTable APIKeyMiddleware --> CourseRoutes APIKeyMiddleware --> AudienceRoutes APIKeyRoutes -->|"session auth"| APIKeyTable ``` ## 1. Database: `api_key` Table Add a new table in [packages/db/src/schema.ts](packages/db/src/schema.ts): ```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), }); ``` - 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. ## 2. API Key Queries New file: [packages/db/src/queries/api-key/api-key.ts](packages/db/src/queries/api-key/api-key.ts) Functions: - `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` ## 3. API Key Auth Middleware New file: [apps/api/src/middlewares/api-key.ts](apps/api/src/middlewares/api-key.ts) - 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 ## 4. Validation Schemas New file: [packages/utils/src/validation/api-key/api-key.ts](packages/utils/src/validation/api-key/api-key.ts) Schemas for API key management and public API input validation: - `ZCreateApiKey` -- `{ name?: string }` - `ZRevokeApiKeyParam` -- `{ keyId: string (uuid) }` - Public API schemas for course CRUD, audience CRUD, pagination, etc. ## 5. Public API Routes (Versioned) ### Router structure New directory: `apps/api/src/routes/v1/` Files: - [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): ```typescript .route('/v1', v1Router) ``` All v1 routes use `apiKeyMiddleware` (not session auth). ### Course Endpoints (`/v1/courses`) | 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 | ### Audience Endpoints (`/v1/audience`) | 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 | ### API Key Management (session-authenticated, not public API) New file: [apps/api/src/routes/api-key/api-key.ts](apps/api/src/routes/api-key/api-key.ts) | 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 | Registered in `app.ts` as `.route('/api-key', apiKeyRouter)`. Protected by `authMiddleware` + `orgAdminMiddleware`. ## 6. Services - [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) ## 7. Tests New test files under `apps/api/src/__tests__/`: - `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 Tests will use Hono's `app.request()` test helper (no HTTP server needed). Mock the DB layer with `vi.mock()`. 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. ## 8. Dashboard UI: Automation Page ### Sidebar Navigation 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`: ```typescript { titleKey: 'org_navigation.automation', path: '/automation', icon: AutomationIcon, // use Zap or Workflow icon from lucide requiresAdmin: true, matchPattern: '^/org/[^/]+/automation(/.*)?$' } ``` ### Route New file: [apps/dashboard/src/routes/org/[slug]/automation/+page.svelte](apps/dashboard/src/routes/org/[slug]/automation/+page.svelte) ### Page Structure ``` +----------------------------------------------+ | Automation | | Connect ClassroomIO with your tools and | | workflows. | +----------------------------------------------+ | [ API ] [ Zapier ] [ Webhooks ] | +----------------------------------------------+ | | | (Tab content based on selection) | | | +----------------------------------------------+ ``` - Title: "Automation" with description text - Uses `Tabs` from `@cio/ui/base/tabs` - Three tabs: **API**, **Zapier**, **Webhooks** ### API Tab Content **Free plan users:** Show `UpgradeBanner` component (same pattern as other pages). Disable all controls. **Paid plan users:** - 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.