feat: read-only API key scoping

New keys can be created as "Full access" (default, unchanged) or
"Read-only" — read-only keys can only make GET/HEAD/OPTIONS requests,
enforced once in requireSessionOrApiKey (lib/api-auth.ts) rather than in
every route, since a route has no way to know a request came from a
scoped key without that check. Existing keys default to full access —
no behavior change for anyone who doesn't opt in.

Also included in this migration: the chat_messages table for the
next commit (chat history persistence) — generated together since both
touched packages/db/src/schema/users.ts in the same pass.

Verified locally: created both a read-only and a full-access key,
confirmed GET succeeds and POST 403s on the read-only key, confirmed
POST still works on the full-access key, and checked the scope badges
render correctly in the real Settings → API Keys UI.
This commit is contained in:
Arnaud
2026-07-12 22:37:14 +02:00
parent b2f2274673
commit 0062220d8e
11 changed files with 5038 additions and 8 deletions
@@ -0,0 +1,16 @@
CREATE TYPE "public"."api_key_scope" AS ENUM('full', 'read');--> statement-breakpoint
CREATE TYPE "public"."chat_role" AS ENUM('user', 'assistant');--> statement-breakpoint
CREATE TABLE "chat_messages" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"recipe_id" text,
"role" "chat_role" NOT NULL,
"content" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "api_keys" ADD COLUMN "scope" "api_key_scope" DEFAULT 'full' NOT NULL;--> statement-breakpoint
ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "chat_messages_user_idx" ON "chat_messages" USING btree ("user_id","created_at");--> statement-breakpoint
CREATE INDEX "chat_messages_recipe_idx" ON "chat_messages" USING btree ("recipe_id");
File diff suppressed because it is too large Load Diff
@@ -246,6 +246,13 @@
"when": 1783875228288,
"tag": "0034_dapper_nocturne",
"breakpoints": true
},
{
"idx": 35,
"version": "7",
"when": 1783887815564,
"tag": "0035_gifted_plazm",
"breakpoints": true
}
]
}