Files
OPENBUREAU/cms/db/schema.sql
T
karim 9163f5c90d security: public Deploy härten (Reverse-Proxy, GoTrue-Rate-Limit, RLS-Revoke)
Für die öffentlich erreichbare Instanz (dev.openbureau.ch):

1. Reverse-Proxy nur /auth/* durchreichen — /rest, /storage, /realtime raus.
   PostgREST /rest/v1/ gab die komplette DB-Schema-Beschreibung (OpenAPI) preis;
   der Browser nutzt Supabase nur fürs Login, Daten laufen über /api/*.
   (Caddy-Block in create-openbureau-lxc.sh + proxmox/README.md angepasst.)
2. GoTrue GOTRUE_RATE_LIMIT_TOKEN_REFRESH=100 — bremst Brute-Force aufs /token,
   das public direkt gegen GoTrue läuft (nicht übers Node-Rate-Limit).
3. db/schema.sql: revoke all from anon/authenticated auf posts/comments/forums/
   threads; grants nur noch service_role. RLS bleibt so auch bei künftigen
   Policies dicht (Defense-in-Depth statt "RLS ohne Policy").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 05:11:19 +02:00

120 lines
5.8 KiB
SQL

-- OPENBUREAU CMS — posts-Tabelle. In den Supabase-Stack einspielen
-- (SQL-Editor oder psql). Spalten bilden das Hugo-Frontmatter ab.
create extension if not exists "pgcrypto";
create table if not exists public.posts (
id uuid primary key default gen_random_uuid(),
section text not null, -- buerofuehrung | software | theorie
slug text not null, -- a-z0-9- (Dateiname ohne .md)
title text not null,
date date not null default current_date,
weight int,
tags text[] default '{}',
summary text,
cover_image text,
layout text, -- z.B. "image" | "text"
external text, -- externer Link (wie RAPPORT)
color text, -- z.B. "kusa" | "yuyake"
body text default '', -- Markdown-Inhalt
status text not null default 'draft', -- draft | published
author text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
published_at timestamptz,
unique (section, slug)
);
create index if not exists posts_status_idx on public.posts (status);
create index if not exists posts_section_idx on public.posts (section);
-- RLS aktivieren; die api nutzt den Service-Key (umgeht RLS). Wenn das
-- Frontend später direkt liest, hier gezielte Policies ergänzen.
alter table public.posts enable row level security;
revoke all on public.posts from anon, authenticated;
-- ── Dialog / Diskussionen ───────────────────────────────────────────────
-- Thread = Pfad des Beitrags (z.B. /library/software/stack/). Flache Wortmeldungen
-- mit optionalem Bezug (parent_id). Idempotent — auf bestehende DB anwendbar.
create table if not exists public.comments (
id uuid primary key default gen_random_uuid(),
thread text not null,
parent_id uuid references public.comments(id) on delete cascade,
user_id uuid,
author_name text,
author_avatar text,
body text not null,
created_at timestamptz not null default now(),
deleted boolean not null default false
);
-- Position/Rolle bei OPENBUREAU (optional, neben dem Namen angezeigt).
alter table public.comments add column if not exists author_role text;
create index if not exists comments_thread_idx on public.comments (thread, created_at);
alter table public.comments enable row level security;
-- Nur die Node-API (service_role) greift auf die Tabellen zu; der Browser geht
-- ausschliesslich über /api/*. anon/authenticated bekommen KEINE Tabellenrechte,
-- damit das öffentlich erreichbare /rest/v1 auch bei künftigen RLS-Policies dicht
-- bleibt (Defense-in-Depth, nicht nur "RLS ohne Policy").
grant all on public.comments to service_role;
revoke all on public.comments from anon, authenticated;
-- Aggregat je Thread (Anzahl + letzte Aktivität). Spart der API den Full-Table-
-- Scan + JS-Aggregation bei jedem Forum-Aufruf; Postgres zählt direkt.
create or replace view public.comment_stats as
select thread, count(*)::int as count, max(created_at) as last
from public.comments
where not deleted
group by thread;
grant select on public.comment_stats to service_role;
-- ── Foren / Subforen ────────────────────────────────────────────────────
-- Kategorien, in denen Threads leben. Admin-verwaltet. `kind=library` ist die
-- Sonder-Kategorie, in der die Library-Beiträge automatisch als Threads landen.
create table if not exists public.forums (
id uuid primary key default gen_random_uuid(),
slug text not null unique,
name text not null,
description text default '',
color text, -- optionale Akzentfarbe
sort int not null default 0,
kind text not null default 'forum', -- forum | library
created_at timestamptz not null default now()
);
alter table public.forums enable row level security;
grant all on public.forums to service_role;
revoke all on public.forums from anon, authenticated;
-- ── Threads (Diskussionen) ──────────────────────────────────────────────
-- key = stabiler Bezeichner, den comments.thread referenziert:
-- Forum-Thread → 't/<uuid>'
-- Library-Beitrag → Beitragspfad (z.B. /library/software/stack/)
create table if not exists public.threads (
id uuid primary key default gen_random_uuid(),
forum_id uuid references public.forums(id) on delete cascade,
key text not null unique,
title text not null,
url text, -- Ziel-Link (Library: Beitragspfad)
kind text not null default 'forum', -- forum | library
author_name text,
user_id uuid,
locked boolean not null default false,
deleted boolean not null default false,
created_at timestamptz not null default now()
);
create index if not exists threads_forum_idx on public.threads (forum_id);
alter table public.threads enable row level security;
grant all on public.threads to service_role;
revoke all on public.threads from anon, authenticated;
-- Seed-Kategorien (idempotent; im Admin umbenenn-/erweiterbar).
insert into public.forums (slug, name, sort, kind) values
('allgemein', 'Allgemein', 10, 'forum'),
('projekte', 'Projekte', 20, 'forum'),
('technik', 'Technik', 30, 'forum'),
('off-topic', 'Off-Topic', 40, 'forum')
on conflict (slug) do nothing;
-- Sonder-Kategorie für die Library-Beiträge.
insert into public.forums (slug, name, sort, kind) values
('beitraege', 'Beiträge', 0, 'library')
on conflict (slug) do nothing;