Vermietungs-Tool
A local web app for a small room-rental business in Austria: guest register, room ledger, occupancy plan, invoices, the monthly tourist-tax report and the quarterly VAT list. Application code only; it ships no data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
45424e6fd6
47 files changed
+6701
No files matched your search
@@ -0,0 +1,49 @@
|
||||
-- 002 audit log: every row a data table gains, loses or changes — before, after, and who wrote it.
|
||||
-- Backups protect against losing the database, not against it being wrong; this shows which rows changed,
|
||||
-- when and by whom, and `python -m app.audit revert` undoes just those. It is also the registration history
|
||||
-- the Meldegesetz asks for: corrections through history, not by overwriting.
|
||||
-- The trigger function runs as the owner (SECURITY DEFINER): the app's role may read the log but neither
|
||||
-- write nor change it (db._grant), and only a table's owner can switch its triggers off.
|
||||
|
||||
CREATE TABLE audit_log (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
at timestamptz NOT NULL DEFAULT now(), -- when the writing transaction began
|
||||
tx bigint NOT NULL DEFAULT pg_current_xact_id()::text::bigint, -- revert one transaction whole
|
||||
actor text COLLATE "C" NOT NULL, -- the connection's app.actor: web | cli | ...
|
||||
db_user text COLLATE "C" NOT NULL DEFAULT session_user, -- the login role, unspoofable
|
||||
table_name text COLLATE "C" NOT NULL,
|
||||
op text COLLATE "C" NOT NULL, -- INSERT | UPDATE | DELETE | TRUNCATE
|
||||
row_id bigint, -- null for company_key / dismissed_finding: their key is in the JSON
|
||||
old jsonb,
|
||||
new jsonb
|
||||
);
|
||||
CREATE INDEX audit_log_row ON audit_log (table_name, row_id);
|
||||
CREATE INDEX audit_log_tx ON audit_log (tx);
|
||||
|
||||
CREATE FUNCTION audit_row() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $$
|
||||
DECLARE
|
||||
o jsonb := CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN to_jsonb(OLD) END;
|
||||
n jsonb := CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW) END;
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE' AND o = n THEN
|
||||
RETURN NULL; -- a save that changed nothing is no history
|
||||
END IF;
|
||||
EXECUTE format('INSERT INTO %I.audit_log (actor, table_name, op, row_id, old, new) '
|
||||
'VALUES ($1, $2, $3, $4, $5, $6)', TG_TABLE_SCHEMA)
|
||||
USING COALESCE(NULLIF(current_setting('app.actor', true), ''), 'unknown'), TG_TABLE_NAME, TG_OP,
|
||||
(COALESCE(n, o) ->> 'id')::bigint, o, n;
|
||||
RETURN NULL;
|
||||
END $$;
|
||||
REVOKE EXECUTE ON FUNCTION audit_row() FROM PUBLIC;
|
||||
|
||||
DO $$
|
||||
DECLARE t text;
|
||||
BEGIN
|
||||
FOREACH t IN ARRAY ARRAY['unit', 'room', 'company', 'client', 'company_key', 'dismissed_finding', 'guest',
|
||||
'invoice', 'stay', 'charge', 'payment'] LOOP
|
||||
EXECUTE format('CREATE TRIGGER %I AFTER INSERT OR UPDATE OR DELETE ON %I '
|
||||
'FOR EACH ROW EXECUTE FUNCTION audit_row()', t || '_audit', t);
|
||||
EXECUTE format('CREATE TRIGGER %I AFTER TRUNCATE ON %I '
|
||||
'FOR EACH STATEMENT EXECUTE FUNCTION audit_row()', t || '_audit_truncate', t);
|
||||
END LOOP;
|
||||
END $$;
|
||||
Reference in new issue
Block a user