The database moved to a container of our own; the platform is gone. This takes out what was left of it — and, where the leftovers were load bearing, moves rather than deletes. Moved, not deleted: supabase/migrations/ -> db/migrations/ the schema's source of truth supabase/build-org.ts -> scripts/build-org.ts lib/supabase/types.ts -> lib/types.ts 52 import sites repointed The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`, and simply renaming the schema would have left the runner facing an empty table: it would have called all 67 migrations pending and replayed them against a database that is long since current. So the runner now creates `migrationen.schema_migrations` and, once, copies the old rows across — guarded so a second run does nothing and a fresh database skips it entirely. Only then does migration 20260907100000 drop the old schema. Deleted: the CLI config, the seed, the historical schema/function dumps (nothing read them), scripts/umzug-von-supabase.sh (the move is done), and both Supabase packages plus the CLI. Nothing in the application imported them — the build now succeeds with no environment variables at all, which is the proof. Integration tests: six of them signed in through Supabase Auth and asserted against the anon key and the service role. That model is gone, so the tests were not portable — they are deleted. session-context and employee-status-filter already ran on pg and are untouched; om-reporting is ported to a direct connection because it guards a real risk (the reporting line rule exists twice, once in SQL and once in TypeScript). CI: the integration job started a Supabase stack. It now runs a postgres service, applies deploy/db-init and every migration to an empty database — that was the valuable part, and it still holds — then checks that a second run is a no-op, which is what proves the bookkeeping works. Docs: security-review.md audited a service-role key, a cookie adapter and auth.users, none of which exist. Restating findings about removed components would suggest today's system had been reviewed; it has not. It now records what was removed and says a fresh review is due. data-model.md was already marked obsolete and described the pre-OM schema; azure-migration.md was a plan for a route not taken. Both deleted. Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the packages. Integration tests skip cleanly with no database. Migration SQL and the runner are reviewed but NOT executed: no Docker here, and the old instance no longer resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.4 KiB
PL/PgSQL
118 lines
4.4 KiB
PL/PgSQL
-- Schritt 2: die Anmeldung braucht einen Weg, ihre Zeile in app_users
|
|
-- anzulegen — bevor es einen Sitzungskontext gibt.
|
|
--
|
|
-- Das ist das Henne-Ei-Problem jeder eigenen Anmeldung: app.user_id kann erst
|
|
-- gesetzt werden, wenn die Kennung feststeht, und die entsteht genau hier.
|
|
-- Bisher löste ein Dienstschlüssel mit BYPASSRLS solche Fälle. Den gibt es
|
|
-- nicht mehr, und er soll auch nicht zurückkommen — eine Verbindung, die
|
|
-- alles darf, ist für einen einzigen Schreibvorgang ein zu grosser Hebel.
|
|
--
|
|
-- Stattdessen: eine SECURITY-DEFINER-Funktion mit genau einer Befugnis.
|
|
-- Sie schreibt in app_users und liest lesend in profiles — sonst nichts. Wer
|
|
-- sie aufruft, bekommt eine UUID zurück und sonst keine Auskunft.
|
|
create or replace function app_upsert_user(
|
|
p_external_id text,
|
|
p_email text,
|
|
p_full_name text
|
|
)
|
|
returns uuid
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public, pg_temp
|
|
as $$
|
|
declare
|
|
v_id uuid;
|
|
begin
|
|
if p_external_id is null or btrim(p_external_id) = '' then
|
|
raise exception 'Externe Kennung fehlt.';
|
|
end if;
|
|
if p_email is null or btrim(p_email) = '' then
|
|
raise exception 'E-Mail-Adresse fehlt.';
|
|
end if;
|
|
|
|
-- Bekanntes Konto: nur nachziehen, was sich beim Anbieter geändert haben
|
|
-- kann. Die Kennung bleibt, auch wenn Name oder Adresse wechseln — daran
|
|
-- hängen Notizen, Entwürfe und Protokolleinträge.
|
|
update app_users
|
|
set email = p_email,
|
|
full_name = coalesce(p_full_name, full_name),
|
|
last_seen_at = now()
|
|
where external_id = p_external_id
|
|
returning id into v_id;
|
|
|
|
if v_id is not null then
|
|
return v_id;
|
|
end if;
|
|
|
|
-- Erstanmeldung. Gibt es zu dieser Adresse bereits ein Profil, wird dessen
|
|
-- Kennung übernommen statt einer neuen: profiles.id ist heute die
|
|
-- auth.users.id, und neun Fremdschlüssel zeigen darauf. Eine frisch
|
|
-- vergebene UUID würde die Person von ihrer eigenen Vorgeschichte trennen
|
|
-- — sie wäre angemeldet, hätte aber weder Rolle noch Freischaltung.
|
|
--
|
|
-- Der Abgleich über die Adresse ist hier vertretbar und sonst nirgends:
|
|
-- die Adresse kommt aus einem von Entra ausgestellten Token, nicht aus
|
|
-- einem Formular. Wer sie behauptet, hat sie bereits bewiesen.
|
|
select p.id into v_id
|
|
from profiles p
|
|
where lower(p.email) = lower(p_email)
|
|
limit 1;
|
|
|
|
v_id := coalesce(v_id, gen_random_uuid());
|
|
|
|
begin
|
|
insert into app_users (id, external_id, email, full_name, last_seen_at)
|
|
values (v_id, p_external_id, p_email, p_full_name, now());
|
|
exception
|
|
when unique_violation then
|
|
-- Zwei gleichzeitige Erstanmeldungen desselben Kontos. Die zweite
|
|
-- findet die Zeile, die die erste gerade angelegt hat.
|
|
select id into v_id from app_users where external_id = p_external_id;
|
|
if v_id is null then
|
|
raise;
|
|
end if;
|
|
end;
|
|
|
|
return v_id;
|
|
end;
|
|
$$;
|
|
|
|
comment on function app_upsert_user(text, text, text) is
|
|
'Legt die app_users-Zeile zur Erstanmeldung an und liefert die Kennung. Übernimmt bei bekannter E-Mail die vorhandene profiles.id.';
|
|
|
|
-- app_users trägt RLS und hat bewusst keine Schreib-Policy: die Anwendung
|
|
-- kommt an die Tabelle nur durch diese Funktion. Ein Fehler im Anwendungscode
|
|
-- kann dort also nichts anlegen, ändern oder löschen.
|
|
do $$
|
|
declare r text;
|
|
begin
|
|
foreach r in array array['anon', 'authenticated', 'service_role'] loop
|
|
if exists (select 1 from pg_roles where rolname = r) then
|
|
execute format('grant execute on function app_upsert_user(text, text, text) to %I', r);
|
|
end if;
|
|
end loop;
|
|
end;
|
|
$$;
|
|
|
|
-- ═══ Gegenprobe ══════════════════════════════════════════════════
|
|
-- Zweimal dieselbe externe Kennung muss dieselbe UUID ergeben. Wäre es nicht
|
|
-- so, bekäme jede Anmeldung ein neues Konto und niemand behielte seine
|
|
-- Rolle — ein Fehler, der sich erst Wochen später als „meine Notizen sind
|
|
-- weg" zeigt.
|
|
do $$
|
|
declare
|
|
v_first uuid;
|
|
v_second uuid;
|
|
v_probe text := 'probe-' || gen_random_uuid()::text;
|
|
begin
|
|
v_first := app_upsert_user(v_probe, v_probe || '@example.invalid', 'Probe');
|
|
v_second := app_upsert_user(v_probe, v_probe || '@example.invalid', 'Probe');
|
|
|
|
if v_first is distinct from v_second then
|
|
raise exception 'app_upsert_user() vergibt bei zweiter Anmeldung eine neue Kennung — Abbruch.';
|
|
end if;
|
|
|
|
delete from app_users where id = v_first;
|
|
end;
|
|
$$;
|