The UI worked but read as an operator console: no public face, no way to describe or sell the thing, and no account surface beyond the node list. This adds the missing halves and reorganises what was already there around the incident, which is the unit of value the rest of the pipeline is built to produce. A shared design system replaces per-page styling: components/ui (Button, Card, Badge, EmptyState, Skeleton, PageHeader), a type scale and shadow set in the Tailwind config, and light-mode tokens in globals.css. The existing html:not(.dark) remap mechanism is extended rather than replaced -- a parallel theming system would have been two sources of truth for the same colours. Public marketing pages (/, /features, /pricing, /faq) load without a session. middleware.ts gained a PUBLIC_PATHS allowlist to permit that; it remains a UX redirect and is still NOT an authorisation boundary, which the comment there says explicitly. Real enforcement is unchanged and still lives server-side in c2-core's auth.py. Chrome switching is done by pathname in ChromeSwitcher instead of by route group, because a route group would have collided on / and forced most of app/ to move for no behavioural gain. Billing and API keys ship as typed stubs, not integrations. lib/billing.ts and lib/apiKeys.ts define the data model and the screens consume it, but every mutating call throws with a message naming the backend route that has to exist first, and the sample data is labelled as sample. Nothing here can charge anyone or mint a real credential -- picking a payment processor and holding its keys is a decision for a human, and a half-wired checkout is worse than an obviously absent one. The severity work from the c2-core change lands here too. severity is now a filter and sort dimension on the incident list rather than decoration, since a busy dispatch channel is only readable if you can collapse it to moderate and above. routine gets a muted treatment because it is the majority of traffic, legacy "unknown" still renders nothing, and TypeBadge handles the new "other" incident type. Severity rendering moved into lib/severity.tsx so the incident list, incident detail and call rows cannot drift apart. Deliberately not touched: calls, map, alerts, nodes, systems, tokens, trips and admin. They already share the palette and stay coherent, and rewriting them would have buried the parts that actually needed to change. No colour tokens were renamed, so nothing regressed there. Verified with tsc --noEmit (npm run typecheck), clean. No runtime verification was possible and none was done. No new environment variables.
74 lines
2.8 KiB
TypeScript
74 lines
2.8 KiB
TypeScript
/**
|
|
* Organization API keys — STUB MODULE, no backend endpoint exists yet.
|
|
*
|
|
* This is a distinct concept from the two API-key-shaped things that already
|
|
* exist server-side:
|
|
* - `node_keys` (Firestore collection) — per-node upload credentials, issued
|
|
* via /nodes/{id}/reissue-key. Not this.
|
|
* - The Discord bot token pool (app/tokens) — Discord bot tokens, not this.
|
|
*
|
|
* This module models organization-level API keys for third-party
|
|
* integrations (a standard SaaS feature) that DRB does not yet expose.
|
|
* Everything below is in-memory demo state so the settings UI has something
|
|
* real to render; nothing here is persisted or capable of authenticating
|
|
* against the real API.
|
|
*
|
|
* TODO(api-keys): to make this real, add to drb-c2-core:
|
|
* - `org_api_keys` Firestore collection: {key_id, org_id, name, key_hash,
|
|
* key_prefix, created_at, last_used_at, created_by_uid, revoked}
|
|
* - POST /org/api-keys → generate, return the raw key ONCE
|
|
* - GET /org/api-keys → list (prefix + metadata only, never the raw key)
|
|
* - DELETE /org/api-keys/{id} → revoke
|
|
* - A new auth path in internal/auth.py that checks `Authorization: Bearer drb_live_…`
|
|
* against `key_hash` (constant-time compare), scoped like a viewer/operator token.
|
|
* Then replace the functions below with c2api calls hitting those routes.
|
|
*/
|
|
|
|
export interface ApiKeyRecord {
|
|
key_id: string;
|
|
name: string;
|
|
/** Only the prefix is ever shown after creation — mirrors how real key systems (Stripe, GitHub) do it. */
|
|
key_prefix: string;
|
|
created_at: string;
|
|
last_used_at: string | null;
|
|
revoked: boolean;
|
|
}
|
|
|
|
// Sample/demo fixture — obviously not real keys, never sent anywhere.
|
|
let DEMO_KEYS: ApiKeyRecord[] = [
|
|
{
|
|
key_id: "demo_key_1",
|
|
name: "Ops dashboard integration",
|
|
key_prefix: "drb_live_sample_4f2a",
|
|
created_at: "2026-07-02T14:00:00.000Z",
|
|
last_used_at: "2026-08-15T09:12:00.000Z",
|
|
revoked: false,
|
|
},
|
|
];
|
|
|
|
export async function listApiKeys(): Promise<ApiKeyRecord[]> {
|
|
return DEMO_KEYS;
|
|
}
|
|
|
|
/**
|
|
* Returns the full (fake) key exactly once, same UX contract a real key
|
|
* issuance flow would have — the raw secret is shown once and never again.
|
|
*/
|
|
export async function createApiKey(name: string): Promise<{ record: ApiKeyRecord; rawKey: string }> {
|
|
const suffix = Math.random().toString(36).slice(2, 10);
|
|
const record: ApiKeyRecord = {
|
|
key_id: `demo_key_${DEMO_KEYS.length + 1}`,
|
|
name,
|
|
key_prefix: `drb_live_sample_${suffix.slice(0, 4)}`,
|
|
created_at: new Date().toISOString(),
|
|
last_used_at: null,
|
|
revoked: false,
|
|
};
|
|
DEMO_KEYS = [...DEMO_KEYS, record];
|
|
return { record, rawKey: `drb_live_sample_${suffix}_DEMO_NOT_A_REAL_KEY` };
|
|
}
|
|
|
|
export async function revokeApiKey(keyId: string): Promise<void> {
|
|
DEMO_KEYS = DEMO_KEYS.map((k) => (k.key_id === keyId ? { ...k, revoked: true } : k));
|
|
}
|