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,248 @@
import { marked } from "marked";
import { getPool } from "../../db/index.js";
import { sortClause, eventSearchClause } from "./query.js";
// Every query is scoped to user_id; the only interpolated SQL is the
// whitelisted sort clause. Agreements are deliberately NOT re-scoped by
// user_id — an event may reference a public (user_id IS NULL) agreement.
export async function listCore(userId, { from, to, q = "", sort, dir, limit = 50, offset = 0 } = {}) {
const client = await getPool();
try {
const params = [userId];
let where = `WHERE e.user_id = $1`;
if (from && to) {
params.push(from, to);
// Half-open: includes the whole `to` day. `<= to::date` would drop
// everything after midnight of that day.
where += ` AND e.created_as_ts >= $${params.length - 1}::date AND e.created_as_ts < ($${params.length}::date + 1)`;
}
if (q) {
params.push(q);
where += ` AND ${eventSearchClause(params.length)}`;
}
const total = (
await client.query(
`SELECT COUNT(*)::int AS total
FROM event e
INNER JOIN agreement agr ON e.agreement_id = agr.id
${where}`,
params,
)
).rows[0].total;
const rows = (
await client.query(
`SELECT
e.event_id_uuid AS "eventUuid",
COALESCE(es.short_name, e.sender_id) AS sender,
COALESCE(er.short_name, e.recipient_id) AS receiver,
e.created_as_ts AS date,
agr.agreement_id_uuid AS "agreementUuid",
ac.title AS "agreementTitle",
octet_length(ed.data::text) AS "size"
FROM event e
INNER JOIN agreement agr ON e.agreement_id = agr.id
LEFT JOIN agreement_content ac ON ac.agreement_uuid = agr.agreement_id_uuid
LEFT JOIN event_data ed ON ed.event_id = e.id
LEFT JOIN entity es ON es.did_id = e.sender_id AND es.user_id = $1
LEFT JOIN entity er ON er.did_id = e.recipient_id AND er.user_id = $1
${where}
${sortClause(sort, dir)}
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
[...params, limit, offset],
)
).rows;
return { rows, total };
} finally {
await client.release();
}
}
// One row per AUDIT RECORD, not per event: `eventUuid` repeats across rows and
// `auditId` is what identifies a row uniquely.
export async function listAudit(userId, { from, to, q = "", sort, dir, limit = 50, offset = 0 } = {}) {
const client = await getPool();
try {
const params = [userId];
let where = `WHERE a.event_id IS NOT NULL`;
if (from && to) {
params.push(from, to);
where += ` AND a.created_ts >= $${params.length - 1}::date AND a.created_ts < ($${params.length}::date + 1)`;
}
if (q) {
params.push(q);
where += ` AND ${eventSearchClause(params.length)}`;
}
const baseFrom = `
FROM audit a
INNER JOIN event e ON e.event_id_uuid = a.event_id AND e.user_id = $1
INNER JOIN agreement agr ON agr.id = e.agreement_id`;
const total = (
await client.query(`SELECT COUNT(*)::int AS total ${baseFrom} ${where}`, params)
).rows[0].total;
const rows = (
await client.query(
`SELECT
a.audit_id AS "auditId",
a.digest AS "auditDigest",
e.event_id_uuid AS "eventUuid",
COALESCE(es.short_name, e.sender_id) AS sender,
COALESCE(er.short_name, e.recipient_id) AS receiver,
a.created_ts AS date,
agr.agreement_id_uuid AS "agreementUuid",
ac.title AS "agreementTitle",
(
octet_length(a.digest)
+ COALESCE((SELECT SUM(octet_length(s.jws)) FROM audit_signature s WHERE s.audit_id = a.audit_id), 0)
) AS "size"
${baseFrom}
LEFT JOIN agreement_content ac ON ac.agreement_uuid = agr.agreement_id_uuid
LEFT JOIN entity es ON es.did_id = e.sender_id AND es.user_id = $1
LEFT JOIN entity er ON er.did_id = e.recipient_id AND er.user_id = $1
${where}
${sortClause(sort, dir)}
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
[...params, limit, offset],
)
).rows;
return { rows, total };
} finally {
await client.release();
}
}
export async function getEvent(userId, eventUuid) {
const client = await getPool();
try {
const record =
(
await client.query(
`SELECT JSON_BUILD_OBJECT(
'version', e.version,
'eventId', e.event_id_uuid,
'type', et.value,
'senderId', e.sender_id,
'recipientId', e.recipient_id,
'created', e.created,
'agreementId', agr.agreement_id_uuid,
'data', ed.data
) AS record
FROM event e
JOIN event_type et ON et.id = e.event_type_id
JOIN agreement agr ON agr.id = e.agreement_id
LEFT JOIN event_data ed ON ed.event_id = e.id
WHERE e.user_id = $1 AND e.event_id_uuid = $2`,
[userId, eventUuid],
)
).rows[0]?.record ?? null;
if (record?.data) {
try {
record.data = JSON.parse(record.data);
} catch {
// leave the raw string if it isn't JSON
}
}
return record;
} finally {
await client.release();
}
}
// Cached agreement markdown rendered to HTML for the "click the agreement" modal.
// Scoped to the viewer: their own agreement or a public (user_id IS NULL) one.
export async function getAgreement(userId, agreementUuid) {
const client = await getPool();
try {
const row = (
await client.query(
`SELECT title, markdown FROM agreement_content
WHERE agreement_uuid = $1 AND (user_id = $2 OR user_id IS NULL)
LIMIT 1`,
[agreementUuid, userId],
)
).rows[0];
if (!row) return null;
return { title: row.title, html: marked(row.markdown || "") };
} finally {
await client.release();
}
}
export async function getEventDetails(userId, eventUuid) {
const client = await getPool();
try {
const event = await getEvent(userId, eventUuid);
if (!event) return null;
const result = await client.query(`
SELECT s.signer_id, r.value AS role
FROM signature s
JOIN agreement agr ON agr.id = s.agreement_id
LEFT JOIN role r ON r.id = s.role_id
WHERE agr.agreement_id_uuid = $1
`, [event.agreementId])
const roleByDid = {};
for (const row of result.rows) {
roleByDid[row.signer_id] = row.role;
}
const roles = {
sender: roleByDid[event.senderId] || null,
receiver: roleByDid[event.recipientId] || null
}
// Resolve the sender/receiver DIDs to their entity short names (user@fedid…).
const nameResult = await client.query(`
SELECT did_id, short_name FROM entity
WHERE user_id = $1 AND did_id = ANY($2::text[])
`, [userId, [event.senderId, event.recipientId]])
const nameByDid = {}
for (const row of nameResult.rows) nameByDid[row.did_id] = row.short_name
const names = {
sender: nameByDid[event.senderId] || null,
receiver: nameByDid[event.recipientId] || null
}
const permittedResult = await client.query(`
SELECT p.value FROM purpose p
INNER JOIN agreement_purpose ap
ON p.id = ap.purpose_id
INNER JOIN agreement a ON ap.agreement_id = a.id
WHERE a.agreement_id_uuid = $1
`, [event.agreementId])
const permitted = permittedResult.rows.map(row => row.value);
const prohibitedResult = await client.query(`
SELECT p.value FROM prohibition p
INNER JOIN agreement_prohibition aph
ON p.id = aph.prohibition_id
INNER JOIN agreement a ON aph.agreement_id = a.id
WHERE a.agreement_id_uuid = $1
`, [event.agreementId])
const prohibited = prohibitedResult.rows.map(row => row.value)
const title = (await client.query(`
SELECT title FROM agreement_content
WHERE agreement_uuid = $1
`, [event.agreementId])).rows[0]?.title ?? null;
return {
event,
roles,
names,
agreement: {permitted, prohibited, title}
}
} finally {
client.release();
}
}