addition of new dashboard

This commit is contained in:
2026-08-20 15:08:32 +00:00
parent 0622018d95
commit bf59249ed7
161 changed files with 13170 additions and 119 deletions

View File

@@ -0,0 +1,4 @@
{
"purposes": [],
"prohibitions": []
}

View File

@@ -0,0 +1,18 @@
JLINC General Audit Agreement
-----------------------------
This document outlines the terms and conditions for using the JLINC protocol for auditing purposes. The purpose of this agreement is to clarify that no formal legal agreements are established between parties beyond their use of the JLINC protocol for data communication auditing.
**Purpose:** This agreement exists solely to provide a framework for using the JLINC protocol to audit data communication among parties. It does not create any legal obligations or enforceable commitments beyond its scope.
**Awareness of Agreement:** Recognizing that some parties may not be aware of this agreement, it is understood that by utilizing the JLINC protocol under this agreement, they are not deemed to have accepted these terms.
**Use of Protocol:** Under this agreement, the JLINC protocol is employed exclusively for auditing data communication, ensuring transparency and efficiency in transactions without establishing any formal agreements beyond this document.
**No Data Provenance/Protection:** It is clarified that no data provenance or protection measures are provided under this agreement. Entities leveraging this agreement are responsible for their own data integrity and security outside the protocol's use.
**Liability:** Liability is limited to actions taken within the context of using the JLINC protocol. No party assumes responsibility for others' actions beyond their direct involvement with the JLINC protocol.
**Governing Law:** This agreement is governed by the laws of the system operator's and user's jurisdictions. Any legal matters arising from this agreement must be resolved in courts within those jurisdictions.
By adhering to these terms, parties acknowledge their responsibilities and agree to use the specified protocol solely for auditing purposes without implied legal obligations beyond those stated herein.

208
backend/db/index.js Normal file
View File

@@ -0,0 +1,208 @@
import fs from "fs";
import path from "path";
import { createHash } from "crypto";
import pkg from "pg";
const { Pool } = pkg;
import { sleep } from "../common/sleep.js";
import { getConfig } from "../common/config.js";
import { firstNonBlankLine } from "../util/firstNonBlankLine.js";
// NOTE: the domain `data` layer (which pulls in @jlinc/core) is imported lazily
// inside populateAgreements() only. Keeping it out of this module's top-level
// imports lets the pool/migrate code — and anything that just needs getPool() —
// load without dragging in the crypto engine.
let pool;
export async function init() {
const config = getConfig();
let ready = false;
let client;
while (!ready) {
try {
pool = new Pool({
connectionString: config.postgresUrl,
});
client = await pool.connect();
const res = await client.query(`SELECT 1`);
if (res.rows.length < 1) {
throw new Error("");
}
ready = true;
// eslint-disable-next-line no-unused-vars
} catch (e) {
console.log("DB not ready, waiting...");
await sleep(1000);
}
}
await client.release();
}
export async function getPool() {
return await pool.connect();
}
export async function close() {
console.log(`Closing DB`);
await pool.end();
}
export async function migrate() {
console.log(`Starting migration`);
const client = await pool.connect();
try {
const migrationExists = await client.query(`
SELECT
CASE
WHEN (SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = 'system' AND table_name = 'migrate') > 0
THEN TRUE
ELSE FALSE
END AS exists
`);
if (!migrationExists.rows[0].exists) {
await client.query(`
DROP SCHEMA IF EXISTS system CASCADE;
CREATE SCHEMA system;
-- Migrations
DROP TABLE IF EXISTS system.migrate CASCADE;
CREATE TABLE system.migrate (
id TEXT NOT NULL,
status TEXT NOT NULL,
created_ts TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx__system__migrate__id ON system.migrate (id);
CREATE INDEX idx__system__migrate__status ON system.migrate (status);
CREATE INDEX idx__system__migrate__created_ts ON system.migrate (created_ts);
`);
}
let lastMigration = "0";
const lastMigrationResult = await client.query(`
SELECT id
FROM system.migrate
ORDER BY id DESC
LIMIT 1;
`);
if (lastMigrationResult.rows.length > 0 && lastMigrationResult.rows[0].id) {
lastMigration = lastMigrationResult.rows[0].id;
}
const files = fs.readdirSync("./db/migrations", {
withFileTypes: true,
});
for await (const file of files) {
const migrationId = file.name.slice(0, 6);
if (migrationId > lastMigration) {
const label = file.name.slice(7, file.name.length - 4);
console.log(`Running migration ${label} (${migrationId})`);
const sqlStr = fs.readFileSync(path.join("./db/migrations", file.name), "utf8");
try {
await client.query("BEGIN");
await client.query(sqlStr);
await client.query(
`
INSERT INTO system.migrate (
id,
status
) VALUES (
$1,
'complete'
);
`,
[migrationId],
);
await client.query("COMMIT");
} catch (e) {
console.error(e);
await client.query("ROLLBACK");
throw new Error("migration error");
}
}
}
} catch (e) {
console.error(e);
} finally {
await client.release();
}
console.log(`Ending migration`);
}
export async function populateAgreements() {
console.log(`Starting agreement population`);
const config = getConfig();
const { data } = await import("../modules/core/data/index.js");
const client = await pool.connect();
try {
const files = fs.readdirSync("./db/agreements", {
withFileTypes: true,
});
for await (const file of files) {
if (file.name.endsWith('.json'))
continue;
const agreementUuid = file.name.slice(0, 36);
const markdown = fs.readFileSync(path.join("./db/agreements", file.name), "utf8").trim();
const json = JSON.parse(fs.readFileSync(path.join("./db/agreements", file.name.replace('.md', '.json')), "utf8").trim());
const title = firstNonBlankLine(markdown);
const hash = createHash('sha256')
.update(markdown)
.digest('hex')
try {
const agreementExists = await client.query(`
SELECT
CASE
WHEN (SELECT COUNT(1) FROM agreement_content WHERE title = $1 AND hash = $2 AND user_id IS NULL) > 0
THEN TRUE
ELSE FALSE
END AS exists
`,
[
title,
hash,
]
);
if (!agreementExists.rows[0].exists) {
console.log(`Adding agreement '${title}' (${agreementUuid})`);
// Agreement content is immutable once inserted (it may already be
// signed), so this is INSERT-only — never an upsert.
await client.query(`
INSERT INTO agreement_content (
title,
markdown,
hash,
key,
agreement_uuid
) VALUES (
$1,
$2,
$3,
$4,
$5
);
`, [
title,
markdown,
hash,
json.key,
agreementUuid,
]);
const agreement = {
uri: `${config.publicCoreUrl}/agreements/${hash}`,
purposes: json.purposes || [],
prohibitions: json.prohibitions || [],
shortNames: [],
validRoles: json.validRoles || [],
}
await data.agreement.create(agreement, null, client, agreementUuid);
}
} catch (e) {
console.error(e);
throw new Error("agreement population error");
}
}
} catch (e) {
console.error(e);
} finally {
await client.release();
}
console.log(`Ending agreement population`);
}

View File

@@ -0,0 +1,45 @@
-- DROP TABLE IF EXISTS public.user CASCADE;
CREATE TABLE public.user (
id BIGSERIAL PRIMARY KEY,
username TEXT,
photo TEXT,
issuer TEXT NOT NULL,
type TEXT NOT NULL,
identifier TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__user__issuer_identifier UNIQUE (issuer, identifier)
);
CREATE INDEX idx__user__username ON public.user (username);
CREATE INDEX idx__user__identifier ON public.user (identifier);
CREATE INDEX idx__user__type ON public.user (type);
CREATE INDEX idx__user__issuer ON public.user (issuer);
CREATE INDEX idx__user__created_ts ON public.user (created_ts);
CREATE INDEX idx__user__updated_ts ON public.user (updated_ts);
-- DROP TABLE IF EXISTS public.app CASCADE;
CREATE TABLE public.app (
id BIGSERIAL PRIMARY KEY,
type TEXT UNIQUE NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__app__type ON public.app (type);
CREATE INDEX idx__app__created_ts ON public.app (created_ts);
CREATE INDEX idx__app__updated_ts ON public.app (updated_ts);
-- DROP TABLE IF EXISTS public.auth CASCADE;
CREATE TABLE public.auth (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
app_id BIGINT NOT NULL REFERENCES public.app(id),
api_key TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__auth__user_id_app_id UNIQUE (user_id, app_id)
);
CREATE INDEX idx__auth__user_id ON public.auth (user_id);
CREATE INDEX idx__auth__app_id ON public.auth (app_id);
CREATE INDEX idx__auth__api_key ON public.auth (api_key);
CREATE INDEX idx__auth__created_ts ON public.auth (created_ts);
CREATE INDEX idx__auth__updated_ts ON public.auth (updated_ts);

View File

@@ -0,0 +1,42 @@
CREATE TYPE HASH_TYPE AS ENUM ('SHA256');
CREATE TYPE SIGNATURE_TYPE AS ENUM ('JWS/JCS');
-- DROP TABLE IF EXISTS public.audit CASCADE;
CREATE TABLE public.audit (
audit_id BIGSERIAL PRIMARY KEY,
version SMALLINT NOT NULL,
event_id UUID,
agreement_id UUID,
hash_type HASH_TYPE NOT NULL,
digest TEXT NOT NULL,
created BIGINT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__audit__event_id ON public.audit (event_id);
CREATE INDEX idx__public__audit__agreement_id ON public.audit (agreement_id);
CREATE INDEX idx__public__audit__created ON public.audit (created);
CREATE INDEX idx__public__audit__created_ts ON public.audit (created_ts);
CREATE INDEX idx__public__audit__updated_ts ON public.audit (updated_ts);
ALTER TABLE public.audit
ADD CONSTRAINT cnst__audit__event_or_agreement
CHECK (num_nonnulls(event_id, agreement_id) = 1);
-- DROP TABLE IF EXISTS public.audit_signature CASCADE;
CREATE TABLE public.audit_signature (
signature_id BIGSERIAL PRIMARY KEY,
audit_id BIGINT NOT NULL REFERENCES public.audit(audit_id),
version SMALLINT NOT NULL,
id TEXT NOT NULL,
signedOn BIGINT NOT NULL,
type SIGNATURE_TYPE NOT NULL,
jws TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__audit_signature__id ON public.audit_signature (id);
CREATE INDEX idx__public__audit_signature__signedOn ON public.audit_signature (signedOn);
CREATE INDEX idx__public__audit_signature__type ON public.audit_signature (type);
CREATE INDEX idx__public__audit_signature__created_ts ON public.audit_signature (created_ts);
CREATE INDEX idx__public__audit_signature__updated_ts ON public.audit_signature (updated_ts);

View File

@@ -0,0 +1,28 @@
-- DROP TABLE IF EXISTS public.usage_url CASCADE;
CREATE TABLE public.usage_url (
id BIGSERIAL PRIMARY KEY,
url TEXT NOT NULL,
module TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__usage_url__url_module UNIQUE (url, module)
);
CREATE INDEX idx__public__usage_url__url ON public.usage_url (url);
CREATE INDEX idx__public__usage_url__module ON public.usage_url (module);
CREATE INDEX idx__public__usage_url__created_ts ON public.usage_url (created_ts);
CREATE INDEX idx__public__usage_url__updated_ts ON public.usage_url (updated_ts);
-- DROP TABLE IF EXISTS public.usage CASCADE;
CREATE TABLE public.usage (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
usage_url_id BIGINT NOT NULL REFERENCES public.usage_url(id),
success BOOLEAN NOT NULL DEFAULT TRUE,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__usage__user_id ON public.usage (user_id);
CREATE INDEX idx__public__usage__usage_url_id ON public.usage (usage_url_id);
CREATE INDEX idx__public__usage__success ON public.usage (success);
CREATE INDEX idx__public__usage__created_ts ON public.usage (created_ts);
CREATE INDEX idx__public__usage__updated_ts ON public.usage (updated_ts);

View File

@@ -0,0 +1,220 @@
-- Agreements
-- DROP TABLE IF EXISTS public.agreement CASCADE;
CREATE TABLE public.agreement (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES public.user(id),
version SMALLINT NOT NULL,
parent TEXT NOT NULL,
agreement_id_uuid UUID UNIQUE NOT NULL,
created BIGINT NOT NULL,
created_as_ts TIMESTAMPTZ NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__agreement__user_id ON public.agreement (user_id);
CREATE INDEX idx__public__agreement__version ON public.agreement (version);
CREATE INDEX idx__public__agreement__parent ON public.agreement (parent);
CREATE INDEX idx__public__agreement__agreement_id_uuid ON public.agreement (agreement_id_uuid);
CREATE INDEX idx__public__agreement__created ON public.agreement (created);
CREATE INDEX idx__public__agreement__created_as_ts ON public.agreement (created_as_ts);
CREATE INDEX idx__public__agreement__created_ts ON public.agreement (created_ts);
CREATE INDEX idx__public__agreement__updated_ts ON public.agreement (updated_ts);
-- DROP TABLE IF EXISTS public.agreement_required_id CASCADE;
CREATE TABLE public.agreement_required_id (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
required_id TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__agreement_required_id UNIQUE (user_id, agreement_id, required_id)
);
CREATE INDEX idx__public__agreement_required_id__user_id ON public.agreement_required_id (user_id);
CREATE INDEX idx__public__agreement_required_id__agreement_id ON public.agreement_required_id (agreement_id);
CREATE INDEX idx__public__agreement_required_id__created_ts ON public.agreement_required_id (created_ts);
CREATE INDEX idx__public__agreement_required_id__updated_ts ON public.agreement_required_id (updated_ts);
-- DROP TABLE IF EXISTS public.purpose CASCADE;
CREATE TABLE public.purpose (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
value TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__purpose UNIQUE (user_id, value)
);
CREATE INDEX idx__public__purpose__user_id ON public.purpose (user_id);
CREATE INDEX idx__public__purpose__value ON public.purpose (value);
CREATE INDEX idx__public__purpose__created_ts ON public.purpose (created_ts);
CREATE INDEX idx__public__purpose__updated_ts ON public.purpose (updated_ts);
-- DROP TABLE IF EXISTS public.agreement_purpose CASCADE;
CREATE TABLE public.agreement_purpose (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
purpose_id BIGINT NOT NULL REFERENCES public.purpose(id),
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__agreement_purpose UNIQUE (user_id, agreement_id, purpose_id)
);
CREATE INDEX idx__public__agreement_purpose__user_id ON public.agreement_purpose (user_id);
CREATE INDEX idx__public__agreement_purpose__agreement_id ON public.agreement_purpose (agreement_id);
CREATE INDEX idx__public__agreement_purpose__purpose_id ON public.agreement_purpose (purpose_id);
CREATE INDEX idx__public__agreement_purpose__created_ts ON public.agreement_purpose (created_ts);
CREATE INDEX idx__public__agreement_purpose__updated_ts ON public.agreement_purpose (updated_ts);
-- DROP TABLE IF EXISTS public.caveat CASCADE;
CREATE TABLE public.caveat (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
value TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__caveat UNIQUE (user_id, value)
);
CREATE INDEX idx__public__caveat__user_id ON public.caveat (user_id);
CREATE INDEX idx__public__caveat__value ON public.caveat (value);
CREATE INDEX idx__public__caveat__created_ts ON public.caveat (created_ts);
CREATE INDEX idx__public__caveat__updated_ts ON public.caveat (updated_ts);
-- DROP TABLE IF EXISTS public.agreement_caveat CASCADE;
CREATE TABLE public.agreement_caveat (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
caveat_id BIGINT NOT NULL REFERENCES public.caveat(id),
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__agreement_caveat UNIQUE (user_id, agreement_id, caveat_id)
);
CREATE INDEX idx__public__agreement_caveat__user_id ON public.agreement_caveat (user_id);
CREATE INDEX idx__public__agreement_caveat__agreement_id ON public.agreement_caveat (agreement_id);
CREATE INDEX idx__public__agreement_caveat__caveat_id ON public.agreement_caveat (caveat_id);
CREATE INDEX idx__public__agreement_caveat__created_ts ON public.agreement_caveat (created_ts);
CREATE INDEX idx__public__agreement_caveat__updated_ts ON public.agreement_caveat (updated_ts);
-- DROP TABLE IF EXISTS public.role CASCADE;
CREATE TABLE public.role (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
value TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__role UNIQUE (user_id, value)
);
CREATE INDEX idx__public__role__user_id ON public.role (user_id);
CREATE INDEX idx__public__role__value ON public.role (value);
CREATE INDEX idx__public__role__created_ts ON public.role (created_ts);
CREATE INDEX idx__public__role__updated_ts ON public.role (updated_ts);
-- DROP TABLE IF EXISTS public.agreement_role CASCADE;
CREATE TABLE public.agreement_role (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
role_id BIGINT NOT NULL REFERENCES public.role(id),
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__agreement_role UNIQUE (user_id, agreement_id, role_id)
);
CREATE INDEX idx__public__agreement_role__user_id ON public.agreement_role (user_id);
CREATE INDEX idx__public__agreement_role__agreement_id ON public.agreement_role (agreement_id);
CREATE INDEX idx__public__agreement_role__role_id ON public.agreement_role (role_id);
CREATE INDEX idx__public__agreement_role__created_ts ON public.agreement_role (created_ts);
CREATE INDEX idx__public__agreement_role__updated_ts ON public.agreement_role (updated_ts);
-- Events
-- DROP TABLE IF EXISTS public.event_type CASCADE;
CREATE TABLE public.event_type (
id BIGSERIAL PRIMARY KEY,
value TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__event_type__value ON public.event_type (value);
CREATE INDEX idx__public__event_type__created_ts ON public.event_type (created_ts);
CREATE INDEX idx__public__event_type__updated_ts ON public.event_type (updated_ts);
INSERT INTO public.event_type (value) VALUES ('data');
-- DROP TABLE IF EXISTS public.event CASCADE;
CREATE TABLE public.event (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
version SMALLINT NOT NULL,
event_id_uuid UUID UNIQUE NOT NULL,
event_type_id BIGINT NOT NULL REFERENCES public.event_type(id),
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
sender_id TEXT NOT NULL,
recipient_id TEXT NOT NULL,
created BIGINT NOT NULL,
created_as_ts TIMESTAMPTZ NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__event__user_id ON public.event (user_id);
CREATE INDEX idx__public__event__version ON public.event (version);
CREATE INDEX idx__public__event__event_id_uuid ON public.event (event_id_uuid);
CREATE INDEX idx__public__event__event_type_id ON public.event (event_type_id);
CREATE INDEX idx__public__event__agreement_id ON public.event (agreement_id);
CREATE INDEX idx__public__event__sender_id ON public.event (sender_id);
CREATE INDEX idx__public__event__recipient_id ON public.event (recipient_id);
CREATE INDEX idx__public__event__created_ts ON public.event (created_ts);
CREATE INDEX idx__public__event__updated_ts ON public.event (updated_ts);
-- DROP TABLE IF EXISTS public.event_data CASCADE;
-- DROP SEQUENCE IF EXISTS seq__event_data__id;
-- CREATE SEQUENCE seq__event_data__id;
-- CREATE TABLE public.event_data (
-- id BIGINT NOT NULL DEFAULT nextval('seq__event_data__id'),
-- user_id BIGINT NOT NULL REFERENCES public.user(id),
-- event_id BIGINT NOT NULL REFERENCES public.event(id),
-- data TEXT NOT NULL,
-- created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- PRIMARY KEY (user_id, id)
-- ) PARTITION BY LIST (user_id);
CREATE TABLE public.event_data (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
event_id BIGINT NOT NULL REFERENCES public.event(id),
data TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__event_data__event_id ON public.event_data (event_id);
CREATE INDEX idx__public__event_data__user_id ON public.event_data (user_id);
CREATE INDEX idx__public__event_data__created_ts ON public.event_data (created_ts);
CREATE INDEX idx__public__event_data__updated_ts ON public.event_data (updated_ts);
-- Signatures
-- DROP TABLE IF EXISTS public.signature CASCADE;
CREATE TABLE public.signature (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
version SMALLINT NOT NULL,
signer_id TEXT NOT NULL,
signed_on BIGINT NOT NULL,
type TEXT NOT NULL,
jws TEXT NOT NULL,
role_id BIGINT REFERENCES public.role(id),
agreement_id BIGINT REFERENCES public.agreement(id),
event_id BIGINT REFERENCES public.event(id),
signed_on_as_ts TIMESTAMPTZ NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__signature__agreement UNIQUE (user_id, signer_id, agreement_id),
CONSTRAINT uniq__signature__event UNIQUE (user_id, signer_id, event_id)
);
CREATE INDEX idx__public__signature__user_id ON public.signature (user_id);
CREATE INDEX idx__public__signature__version ON public.signature (version);
CREATE INDEX idx__public__signature__signed_on ON public.signature (signed_on);
CREATE INDEX idx__public__signature__role_id ON public.signature (role_id);
CREATE INDEX idx__public__signature__agreement_id ON public.signature (agreement_id);
CREATE INDEX idx__public__signature__event_id ON public.signature (event_id);
CREATE INDEX idx__public__signature__signed_on_as_ts ON public.signature (signed_on_as_ts);
CREATE INDEX idx__public__signature__created_ts ON public.signature (created_ts);
CREATE INDEX idx__public__signature__updated_ts ON public.signature (updated_ts);

View File

@@ -0,0 +1,19 @@
-- DROP TABLE IF EXISTS public.entity CASCADE;
CREATE TABLE public.entity (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
fedid_url TEXT NOT NULL,
short_name TEXT NOT NULL,
did_id TEXT NOT NULL,
control_private_key_b64u TEXT NOT NULL,
recovery_private_key_b64u TEXT NOT NULL,
did_doc JSONB NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__entity__user_id ON public.entity (user_id);
CREATE INDEX idx__public__entity__fedid_url ON public.entity (fedid_url);
CREATE INDEX idx__public__entity__short_name ON public.entity (short_name);
CREATE INDEX idx__public__entity__did_id ON public.entity (did_id);
CREATE INDEX idx__public__entity__created_ts ON public.entity (created_ts);
CREATE INDEX idx__public__entity__updated_ts ON public.entity (updated_ts);

View File

@@ -0,0 +1,14 @@
-- DROP TABLE IF EXISTS public.agreement_content CASCADE;
CREATE TABLE public.agreement_content (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES public.user(id),
title TEXT NOT NULL,
markdown TEXT NOT NULL,
hash TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__agreement_content UNIQUE NULLS NOT DISTINCT (user_id, title)
);
CREATE INDEX idx__public__agreement_content__user_id ON public.agreement_content (user_id);
CREATE INDEX idx__public__agreement_content__created_ts ON public.agreement_content (created_ts);
CREATE INDEX idx__public__agreement_content__updated_ts ON public.agreement_content (updated_ts);

View File

@@ -0,0 +1 @@
CREATE INDEX idx__public__audit_signature__audit_id ON public.audit_signature (audit_id);

View File

@@ -0,0 +1,31 @@
-- DROP TABLE IF EXISTS public.event_meta CASCADE;
CREATE TABLE public.event_meta (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
event_id BIGINT NOT NULL REFERENCES public.event(id),
key TEXT NOT NULL,
value TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__event_meta__event_id ON public.event_meta (event_id);
CREATE INDEX idx__public__event_meta__user_id ON public.event_meta (user_id);
CREATE INDEX idx__public__event_meta__key ON public.event_meta (key);
CREATE INDEX idx__public__event_meta__value ON public.event_meta (value);
CREATE INDEX idx__public__event_meta__created_ts ON public.event_meta (created_ts);
CREATE INDEX idx__public__event_meta__updated_ts ON public.event_meta (updated_ts);
-- DROP TABLE IF EXISTS public.audit_meta CASCADE;
CREATE TABLE public.audit_meta (
id BIGSERIAL PRIMARY KEY,
audit_id BIGINT NOT NULL REFERENCES public.audit(audit_id),
key TEXT NOT NULL,
value TEXT NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__audit_meta__audit_id ON public.audit_meta (audit_id);
CREATE INDEX idx__public__audit_meta__key ON public.audit_meta (key);
CREATE INDEX idx__public__audit_meta__value ON public.audit_meta (value);
CREATE INDEX idx__public__audit_meta__created_ts ON public.audit_meta (created_ts);
CREATE INDEX idx__public__audit_meta__updated_ts ON public.audit_meta (updated_ts);

View File

@@ -0,0 +1,45 @@
-- DROP TABLE IF EXISTS public.queue_type CASCADE;
CREATE TABLE public.queue_type (
id BIGSERIAL PRIMARY KEY,
value TEXT NOT NULL UNIQUE,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__queue_type__value ON public.queue_type (value);
CREATE INDEX idx__public__queue_type__created_ts ON public.queue_type (created_ts);
CREATE INDEX idx__public__queue_type__updated_ts ON public.queue_type (updated_ts);
INSERT INTO public.queue_type (value) VALUES ('audit') ON CONFLICT DO NOTHING;
-- DROP TABLE IF EXISTS public.queue_url CASCADE;
CREATE TABLE public.queue_url (
id BIGSERIAL PRIMARY KEY,
value TEXT NOT NULL UNIQUE,
next_run_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__queue_url__value ON public.queue_url (value);
CREATE INDEX idx__public__queue_url__next_run_ts ON public.queue_url (next_run_ts);
CREATE INDEX idx__public__queue_url__created_ts ON public.queue_url (created_ts);
CREATE INDEX idx__public__queue_url__updated_ts ON public.queue_url (updated_ts);
-- DROP TABLE IF EXISTS public.queue CASCADE;
CREATE TABLE public.queue (
id BIGSERIAL PRIMARY KEY,
queue_type_id BIGINT NOT NULL REFERENCES public.queue_type(id),
queue_url_id BIGINT NOT NULL REFERENCES public.queue_url(id),
headers JSON,
data JSON,
run_count BIGINT NOT NULL DEFAULT 0,
last_fail TEXT,
last_run_ts TIMESTAMPTZ,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__queue__queue_type_id ON public.queue (queue_type_id);
CREATE INDEX idx__public__queue__run_count ON public.queue (run_count);
CREATE INDEX idx__public__queue__last_run_ts ON public.queue (last_run_ts);
CREATE INDEX idx__public__queue__created_ts ON public.queue (created_ts);
CREATE INDEX idx__public__queue__updated_ts ON public.queue (updated_ts);

View File

@@ -0,0 +1,11 @@
DO $$ BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'agreement'
AND column_name = 'user_id'
AND is_nullable = 'NO'
) THEN
EXECUTE 'ALTER TABLE agreement ALTER COLUMN user_id DROP NOT NULL';
END IF;
END $$;

View File

@@ -0,0 +1,46 @@
ALTER TABLE public.caveat RENAME TO prohibition;
ALTER TABLE public.prohibition RENAME CONSTRAINT uniq__caveat TO uniq__prohibition;
ALTER INDEX idx__public__caveat__user_id RENAME TO idx__public__prohibition__user_id;
ALTER INDEX idx__public__caveat__value RENAME TO idx__public__prohibition__value;
ALTER INDEX idx__public__caveat__created_ts RENAME TO idx__public__prohibition__created_ts;
ALTER INDEX idx__public__caveat__updated_ts RENAME TO idx__public__prohibition__updated_ts;
ALTER TABLE public.agreement_caveat RENAME TO agreement_prohibition;
ALTER TABLE public.agreement_prohibition RENAME CONSTRAINT uniq__agreement_caveat TO uniq__agreement_prohibition;
ALTER TABLE public.agreement_prohibition RENAME COLUMN caveat_id TO prohibition_id;
ALTER TABLE public.agreement_prohibition
DROP CONSTRAINT IF EXISTS agreement_caveat_caveat_id_fkey,
ADD CONSTRAINT agreement_prohibition_prohibition_id_fkey
FOREIGN KEY (prohibition_id)
REFERENCES public.prohibition(id);
ALTER INDEX idx__public__agreement_caveat__user_id RENAME TO idx__public__agreement_prohibition__user_id;
ALTER INDEX idx__public__agreement_caveat__agreement_id RENAME TO idx__public__agreement_prohibition__agreement_id;
ALTER INDEX idx__public__agreement_caveat__caveat_id RENAME TO idx__public__agreement_prohibition__prohibition_id;
ALTER INDEX idx__public__agreement_caveat__created_ts RENAME TO idx__public__agreement_prohibition__created_ts;
ALTER INDEX idx__public__agreement_caveat__updated_ts RENAME TO idx__public__agreement_prohibition__updated_ts;
ALTER TABLE agreement_content ADD COLUMN key VARCHAR(255) UNIQUE;
CREATE INDEX idx__agreement_content__key ON public.agreement_content (key);
ALTER TABLE agreement_content ADD COLUMN agreement_uuid UUID UNIQUE NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';
ALTER TABLE purpose ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE prohibition ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE role ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE agreement_purpose ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE agreement_prohibition ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE agreement_role ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE public.purpose DROP CONSTRAINT uniq__purpose;
CREATE UNIQUE INDEX uniq__purpose ON public.purpose (COALESCE(user_id, -1), value);
ALTER TABLE public.prohibition DROP CONSTRAINT uniq__prohibition;
CREATE UNIQUE INDEX uniq__prohibition ON public.prohibition (COALESCE(user_id, -1), value);
ALTER TABLE public.role DROP CONSTRAINT uniq__role;
CREATE UNIQUE INDEX uniq__role ON public.role (COALESCE(user_id, -1), value);
ALTER TABLE public.agreement_purpose DROP CONSTRAINT uniq__agreement_purpose;
CREATE UNIQUE INDEX uniq__agreement_purpose ON public.agreement_purpose (COALESCE(user_id, -1), agreement_id, purpose_id);
ALTER TABLE public.agreement_prohibition DROP CONSTRAINT uniq__agreement_prohibition;
CREATE UNIQUE INDEX uniq__agreement_prohibition ON public.agreement_prohibition (COALESCE(user_id, -1), agreement_id, prohibition_id);
ALTER TABLE public.agreement_role DROP CONSTRAINT uniq__agreement_role;
CREATE UNIQUE INDEX uniq__agreement_role ON public.agreement_role (COALESCE(user_id, -1), agreement_id, role_id);
ALTER TABLE public.agreement ADD COLUMN context VARCHAR(255) DEFAULT 'https://protocol.jlinc.org/context/jlinc-v7.jsonld';

View File

@@ -0,0 +1 @@
ALTER TABLE public.agreement ALTER COLUMN context SET DEFAULT NULL;

View File

@@ -0,0 +1,29 @@
-- DROP TABLE IF EXISTS public.reference CASCADE;
CREATE TABLE public.reference (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
uri TEXT NOT NULL UNIQUE,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx__public__reference__user_id ON public.reference (user_id);
CREATE INDEX idx__public__reference__created_ts ON public.reference (created_ts);
CREATE INDEX idx__public__reference__updated_ts ON public.reference (updated_ts);
-- DROP TABLE IF EXISTS public.agreement_reference CASCADE;
CREATE TABLE public.agreement_reference (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
agreement_id BIGINT NOT NULL REFERENCES public.agreement(id),
reference_id BIGINT NOT NULL REFERENCES public.reference(id),
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__agreement_reference_id UNIQUE (user_id, agreement_id, reference_id)
);
CREATE INDEX idx__public__agreement_reference__user_id ON public.agreement_reference (user_id);
CREATE INDEX idx__public__agreement_reference__agreement_id ON public.agreement_reference (agreement_id);
CREATE INDEX idx__public__agreement_reference__reference_id ON public.agreement_reference (reference_id);
CREATE INDEX idx__public__agreement_reference__created_ts ON public.agreement_reference (created_ts);
CREATE INDEX idx__public__agreement_reference__updated_ts ON public.agreement_reference (updated_ts);
ALTER TABLE public.agreement ALTER COLUMN parent DROP NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE public.agreement ADD COLUMN public BOOLEAN DEFAULT FALSE;

View File

@@ -0,0 +1,3 @@
ALTER TABLE public.reference DROP CONSTRAINT reference_uri_key;
ALTER TABLE public.reference ADD CONSTRAINT uniq__agreement_reference_uri_user_id UNIQUE (uri, user_id);
CREATE INDEX idx__public__reference__uri ON public.reference (uri);

View File

@@ -0,0 +1,11 @@
-- Session store for express-session (see http/sessionStore.js).
CREATE TABLE IF NOT EXISTS public.session (
sid VARCHAR NOT NULL PRIMARY KEY,
sess JSONB NOT NULL,
expire TIMESTAMPTZ NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx__public__session__expire ON public.session (expire);
CREATE INDEX IF NOT EXISTS idx__public__session__created_ts ON public.session (created_ts);
CREATE INDEX IF NOT EXISTS idx__public__session__updated_ts ON public.session (updated_ts);

View File

@@ -0,0 +1,14 @@
-- Per-tenant read-through cache; updated_ts drives TTL freshness.
CREATE TABLE IF NOT EXISTS public.cache (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
tag TEXT NOT NULL,
data JSONB NOT NULL,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uniq__cache__user_id_tag UNIQUE (user_id, tag)
);
CREATE INDEX IF NOT EXISTS idx__public__cache__user_id ON public.cache (user_id);
CREATE INDEX IF NOT EXISTS idx__public__cache__tag ON public.cache (tag);
CREATE INDEX IF NOT EXISTS idx__public__cache__created_ts ON public.cache (created_ts);
CREATE INDEX IF NOT EXISTS idx__public__cache__updated_ts ON public.cache (updated_ts);

View File

@@ -0,0 +1,21 @@
-- User-managed API keys. key_hash is a salted scrypt digest, so lookups go via
-- the indexed key_prefix rather than the hash.
CREATE TABLE IF NOT EXISTS public.api_key (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES public.user(id),
app_id BIGINT NOT NULL REFERENCES public.app(id),
label TEXT,
key_hash TEXT NOT NULL,
key_prefix TEXT NOT NULL,
expires_ts TIMESTAMPTZ,
last_used_ts TIMESTAMPTZ,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx__public__api_key__key_prefix ON public.api_key (key_prefix);
CREATE INDEX IF NOT EXISTS idx__public__api_key__user_id ON public.api_key (user_id);
CREATE INDEX IF NOT EXISTS idx__public__api_key__app_id ON public.api_key (app_id);
CREATE INDEX IF NOT EXISTS idx__public__api_key__expires_ts ON public.api_key (expires_ts);
CREATE INDEX IF NOT EXISTS idx__public__api_key__last_used_ts ON public.api_key (last_used_ts);
CREATE INDEX IF NOT EXISTS idx__public__api_key__created_ts ON public.api_key (created_ts);
CREATE INDEX IF NOT EXISTS idx__public__api_key__updated_ts ON public.api_key (updated_ts);

View File

@@ -0,0 +1,6 @@
ALTER TABLE public.usage
ADD COLUMN IF NOT EXISTS event_id_uuid UUID,
ADD COLUMN IF NOT EXISTS agreement_id_uuid UUID;
CREATE INDEX IF NOT EXISTS idx__public__usage__event_id_uuid ON public.usage (event_id_uuid);
CREATE INDEX IF NOT EXISTS idx__public__usage__agreement_id_uuid ON public.usage (agreement_id_uuid);

View File

@@ -0,0 +1,30 @@
CREATE TABLE IF NOT EXISTS public.user_type (
id BIGSERIAL PRIMARY KEY,
value TEXT NOT NULL,
can_view_json BOOLEAN NOT NULL DEFAULT FALSE,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- value is the natural key (the unique index also backs the ON CONFLICT below).
CREATE UNIQUE INDEX IF NOT EXISTS idx__public__user_type__value ON public.user_type (value);
CREATE INDEX IF NOT EXISTS idx__public__user_type__created_ts ON public.user_type (created_ts);
CREATE INDEX IF NOT EXISTS idx__public__user_type__updated_ts ON public.user_type (updated_ts);
-- can_view_json is the capability the app reads; roles are defined only here.
INSERT INTO public.user_type (value, can_view_json)
VALUES ('administrator', TRUE), ('full', TRUE), ('standard', FALSE)
ON CONFLICT (value) DO NOTHING;
ALTER TABLE public.user
ADD COLUMN IF NOT EXISTS user_type_id BIGINT REFERENCES public.user_type(id);
CREATE INDEX IF NOT EXISTS idx__public__user__user_type_id ON public.user (user_type_id);
-- Backfill: every existing user becomes an administrator (safe to re-run: only NULLs).
UPDATE public.user
SET user_type_id = (SELECT id FROM public.user_type WHERE value = 'administrator')
WHERE user_type_id IS NULL;
-- All rows are populated and checkUser() always supplies it going forward.
ALTER TABLE public.user ALTER COLUMN user_type_id SET NOT NULL;

View File

@@ -0,0 +1,2 @@
ALTER TABLE public.agreement_content ADD COLUMN IF NOT EXISTS remote_url TEXT;
CREATE INDEX IF NOT EXISTS idx__agreement_content__remote_url ON public.agreement_content(remote_url);

View File

@@ -0,0 +1,15 @@
-- Retry bookkeeping for remote agreement-markdown fetches. Kept separate from
-- agreement_content (which is immutable once inserted) because this state is
-- mutable: attempts and the next scheduled try are updated on each failure and
-- the row is deleted once the content is successfully cached.
CREATE TABLE IF NOT EXISTS public.agreement_fetch (
agreement_uuid UUID PRIMARY KEY,
remote_url TEXT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
next_attempt_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_error TEXT,
created_ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx__public__agreement_fetch__next_attempt_ts ON public.agreement_fetch (next_attempt_ts);