diff options
| author | Fuwn <[email protected]> | 2026-02-03 23:41:22 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-04 00:29:28 -0800 |
| commit | 3ae36eeadd8b1f6287ed55060b46ce82947b56b7 (patch) | |
| tree | 51b0183580aaf12ccb80c0792cc5b185994179ce /supabase | |
| parent | style: Organise imports across packages (diff) | |
| download | archived-imemio-3ae36eeadd8b1f6287ed55060b46ce82947b56b7.tar.xz archived-imemio-3ae36eeadd8b1f6287ed55060b46ce82947b56b7.zip | |
feat: Add API key authentication for MCP server
Diffstat (limited to 'supabase')
| -rw-r--r-- | supabase/functions/validate-api-key/index.ts | 73 | ||||
| -rw-r--r-- | supabase/migrations/20260204000001_initial_schema.sql (renamed from supabase/migrations/001_initial_schema.sql) | 0 | ||||
| -rw-r--r-- | supabase/migrations/20260204000002_variable_embedding_dimensions.sql (renamed from supabase/migrations/002_variable_embedding_dimensions.sql) | 0 | ||||
| -rw-r--r-- | supabase/migrations/20260204000003_api_keys.sql | 64 |
4 files changed, 137 insertions, 0 deletions
diff --git a/supabase/functions/validate-api-key/index.ts b/supabase/functions/validate-api-key/index.ts new file mode 100644 index 0000000..d426b7e --- /dev/null +++ b/supabase/functions/validate-api-key/index.ts @@ -0,0 +1,73 @@ +import { createClient } from "jsr:@supabase/supabase-js@2"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": + "authorization, x-client-info, apikey, content-type", +}; + +Deno.serve(async (request) => { + if (request.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); + } + + try { + const { apiKey } = await request.json(); + + if (!apiKey || !apiKey.startsWith("imemio_")) { + return new Response(JSON.stringify({ error: "Invalid API key format" }), { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const encoder = new TextEncoder(); + const data = encoder.encode(apiKey); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const keyHash = hashArray + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); + const supabaseUrl = Deno.env.get("SUPABASE_URL"); + const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); + + if (!supabaseUrl || !supabaseServiceKey) { + return new Response( + JSON.stringify({ error: "Server configuration error" }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + const supabase = createClient(supabaseUrl, supabaseServiceKey); + const { data: result, error } = await supabase.rpc("validate_api_key", { + api_key_hash: keyHash, + }); + + if (error || !result || result.length === 0) { + return new Response( + JSON.stringify({ error: "Invalid or revoked API key" }), + { + status: 401, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + const { user_id: userId, api_key_id: apiKeyId } = result[0]; + + supabase.rpc("update_api_key_last_used", { p_api_key_id: apiKeyId }); + + return new Response(JSON.stringify({ userId, apiKeyId }), { + status: 200, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } catch { + return new Response(JSON.stringify({ error: "Internal server error" }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } +}); diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/20260204000001_initial_schema.sql index 09b028e..09b028e 100644 --- a/supabase/migrations/001_initial_schema.sql +++ b/supabase/migrations/20260204000001_initial_schema.sql diff --git a/supabase/migrations/002_variable_embedding_dimensions.sql b/supabase/migrations/20260204000002_variable_embedding_dimensions.sql index 522df47..522df47 100644 --- a/supabase/migrations/002_variable_embedding_dimensions.sql +++ b/supabase/migrations/20260204000002_variable_embedding_dimensions.sql diff --git a/supabase/migrations/20260204000003_api_keys.sql b/supabase/migrations/20260204000003_api_keys.sql new file mode 100644 index 0000000..515deb4 --- /dev/null +++ b/supabase/migrations/20260204000003_api_keys.sql @@ -0,0 +1,64 @@ +create table public.api_keys ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null references public.users(id) on delete cascade, + name text not null, + key_prefix text not null, + key_hash text not null unique, + last_used_at timestamptz, + expires_at timestamptz, + revoked_at timestamptz, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index api_keys_user_id_idx on public.api_keys(user_id); +create index api_keys_key_hash_idx on public.api_keys(key_hash); + +create trigger api_keys_updated_at + before update on public.api_keys + for each row execute function public.handle_updated_at(); + +alter table public.api_keys enable row level security; + +create policy "Users can view own API keys" + on public.api_keys for select + using (auth.uid() = user_id); + +create policy "Users can create own API keys" + on public.api_keys for insert + with check (auth.uid() = user_id); + +create policy "Users can update own API keys" + on public.api_keys for update + using (auth.uid() = user_id); + +create policy "Users can delete own API keys" + on public.api_keys for delete + using (auth.uid() = user_id); + +create or replace function public.validate_api_key(api_key_hash text) +returns table (user_id uuid, api_key_id uuid) +language plpgsql +security definer +as $$ +begin + return query + select ak.user_id, ak.id as api_key_id + from public.api_keys ak + where ak.key_hash = api_key_hash + and ak.revoked_at is null + and (ak.expires_at is null or ak.expires_at > now()); +end; +$$; + +create or replace function public.update_api_key_last_used(p_api_key_id uuid) +returns void +language plpgsql +security definer +as $$ +begin + update public.api_keys + set last_used_at = now() + where id = p_api_key_id; +end; +$$; |