mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-08-03 19:17:07 -04:00

- Adds a new test suite with Docker-based smoke tests for all locales. Can be run using the ./smoketest.sh script. - Replaces all calls to Math.random() with a new helper that returns 0.5 in snapshot testing mode, ensuring deterministic snapshots. - Similarly replaces all calls to new Date() and Date.now() with new helpers that return a fixed date in snapshot testing mode. - Replaces checks against NODE_ENV with APP_ENV, to ensure that the bundles can be built with Nuxt for testing without losing code that would otherwise be stripped out by production optimizations. - Adds a database init script that can be used to initialize the database with a single admin user and a long-lived JWT token for use in automation tests. - Adds a JWT decoding/encoding CLI tool for debugging JWTs. Note: Snapshots are not checked in, and must be generated manually. See test/__snapshots__/.gitignore for more information.
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
import zlib from 'node:zlib';
|
|
import { promisify } from 'util';
|
|
|
|
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 { env } from './env.ts';
|
|
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;
|
|
export 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 (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 };
|
|
}));
|
|
};
|