addition of new dashboard
This commit is contained in:
248
backend/modules/dashboard/transactions.js
Normal file
248
backend/modules/dashboard/transactions.js
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user