249 lines
8.3 KiB
JavaScript
249 lines
8.3 KiB
JavaScript
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();
|
|
}
|
|
}
|