366 lines
15 KiB
Markdown
366 lines
15 KiB
Markdown
---
|
|
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.
|