PronounsPage/server/audit.ts
2024-12-31 19:59:56 +01:00

95 lines
3.1 KiB
TypeScript

import { promisify } from 'util';
import zlib from 'zlib';
import * as Sentry from '@sentry/node';
import SQL from 'sql-template-strings';
import * as sqlite from 'sqlite';
import sqlite3 from 'sqlite3';
import { ulid } from 'ulid';
import type { UserRow } from './express/user.ts';
import { rootDir } from '~/server/paths.ts';
interface Payload {
userId?: string;
[key: string]: unknown;
}
let connection: sqlite.Database | null = null;
const connect = async (): Promise<sqlite.Database> => {
if (!connection) {
connection = await sqlite.open({
filename: `${rootDir}/audit.sqlite`,
driver: sqlite3.Database,
});
// we don't want to replicate the migration setup just for this, so a little workaround here.
// it's already executed on live db, so only creating the table on dev
if (process.env.NODE_ENV === 'development') {
await connection.exec(`
CREATE TABLE IF NOT EXISTS audit_log
(
id TEXT NOT NULL PRIMARY KEY,
userId TEXT,
username TEXT,
aboutUserId TEXT NULL,
event TEXT NOT NULL,
payload BLOB NULL
);
CREATE INDEX IF NOT EXISTS "audit_log_userId" ON "audit_log" ("userId");
CREATE INDEX IF NOT EXISTS "audit_log_username" ON "audit_log" ("username");
CREATE INDEX IF NOT EXISTS "audit_log_aboutUserId" ON "audit_log" ("aboutUserId");
`);
}
}
return connection;
};
export const closeAuditLogConnection = async () => {
if (connection) {
await connection.close();
connection = null;
}
};
const gzip = promisify(zlib.gzip);
const gunzip = promisify(zlib.gunzip);
export const auditLog = async (
req: { user?: Pick<UserRow, 'id' | 'username'> | null; rawUser?: Pick<UserRow, 'id' | 'username'> | undefined },
event: string,
payload: Payload | null = null,
): Promise<void> => {
const db = await connect();
try {
const compressedPayload = payload
? await gzip(Buffer.from(JSON.stringify(payload)) as Uint8Array)
: null;
const user = req.user || req.rawUser || { id: null, username: null };
await db.get(SQL`INSERT INTO audit_log (id, userId, aboutUserId, username, event, payload) VALUES (
${ulid()}, ${user.id}, ${payload?.userId || null}, ${user.username}, ${event}, ${compressedPayload}
)`);
} catch (error) {
Sentry.captureException(error);
}
};
export const fetchAuditLog = async (username: string, userId: string, aboutUserId: string) => {
const db = await connect();
const entries = await db.all(SQL`
SELECT * FROM audit_log
WHERE username = ${username} OR userId = ${userId} OR aboutUserId = ${aboutUserId}
ORDER BY id DESC
`);
return await Promise.all(entries.map(async (entry) => {
const payload = entry.payload ? JSON.parse((await gunzip(entry.payload)).toString()) : null;
return { ...entry, payload };
}));
};