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 => { 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 | null; rawUser?: Pick | undefined }, event: string, payload: Payload | null = null, ): Promise => { 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 }; })); };