301c477e25
Main är sedan den här grenen skapades en helt annan produkt — Semantika,
en mobilapp med egen CDK-infrastruktur. Den äger nu repots rot: en
npm-workspaces-monorepo med apps/mobile, services/api och infra.
För att båda ska rymmas i samma repo flyttar Guidad Felsökning in i en
egen katalog i stället för att göra anspråk på roten:
felsokning/app webbklienten (Vite, egen package.json och
eslint-/vitest-konfiguration)
felsokning/services plattformstjänsten och AI-orkestern
felsokning/infra Terraform och databasschemat
felsokning/docs vision, moduler, drift
felsokning/supabase edge-funktion och migrationer
Merge:n hade tagit bort 128 filer som Guidad Felsökning bygger på —
värdapplikationens komponenter, Supabase-klienten, tillgångar — eftersom
main raderat dem och den här grenen inte råkat ändra just dem. De är
återställda på sin nya plats. Utan dem gick varken bygget eller
testerna: ai.ts och synk.ts importerar Supabase-klienten.
Semantikas rotfiler är orörda: package.json, eslint.config.js och
.github/workflows/ är deras. Guidad Felsökning har egna motsvarigheter i
sin katalog.
CI flyttar samtidigt från GitHub Actions till .gitea/workflows — samma
syntax, egna runners. .github/workflows/ tillhör Semantika härefter.
Verifierat på den nya platsen: 96 vitest-tester, typkontroll, eslint på
både klient och tjänster, bygge, och integrationstest mot riktig Postgres.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
75 lines
2.2 KiB
TypeScript
75 lines
2.2 KiB
TypeScript
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0";
|
|
|
|
/**
|
|
* Verifies the caller is an authenticated admin.
|
|
* Returns { ok: true } on success or { ok: false, response } with a
|
|
* ready-to-return Response on failure.
|
|
*/
|
|
export async function requireAdmin(
|
|
req: Request,
|
|
corsHeaders: Record<string, string>,
|
|
): Promise<{ ok: true; userId: string } | { ok: false; response: Response }> {
|
|
const authHeader = req.headers.get("Authorization");
|
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
return {
|
|
ok: false,
|
|
response: new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
status: 401,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
}),
|
|
};
|
|
}
|
|
|
|
const supabaseUrl = Deno.env.get("SUPABASE_URL");
|
|
const anonKey = Deno.env.get("SUPABASE_ANON_KEY");
|
|
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
|
if (!supabaseUrl || !anonKey || !serviceKey) {
|
|
return {
|
|
ok: false,
|
|
response: new Response(JSON.stringify({ error: "Server misconfigured" }), {
|
|
status: 500,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
}),
|
|
};
|
|
}
|
|
|
|
const supabase = createClient(supabaseUrl, anonKey, {
|
|
global: { headers: { Authorization: authHeader } },
|
|
});
|
|
|
|
const token = authHeader.replace("Bearer ", "");
|
|
const { data, error } = await supabase.auth.getClaims(token);
|
|
if (error || !data?.claims?.sub) {
|
|
return {
|
|
ok: false,
|
|
response: new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
status: 401,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
}),
|
|
};
|
|
}
|
|
|
|
const userId = data.claims.sub as string;
|
|
|
|
// Verify admin role via service-role client (bypasses RLS deterministically)
|
|
const admin = createClient(supabaseUrl, serviceKey);
|
|
const { data: role, error: roleErr } = await admin
|
|
.from("user_roles")
|
|
.select("role")
|
|
.eq("user_id", userId)
|
|
.eq("role", "admin")
|
|
.maybeSingle();
|
|
|
|
if (roleErr || !role) {
|
|
return {
|
|
ok: false,
|
|
response: new Response(JSON.stringify({ error: "Forbidden" }), {
|
|
status: 403,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
}),
|
|
};
|
|
}
|
|
|
|
return { ok: true, userId };
|
|
}
|