mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-18 12:05:28 -04:00
audit log
This commit is contained in:
parent
7ac68dcd1f
commit
cf5a868f78
@ -13,13 +13,13 @@
|
||||
</td>
|
||||
</tr>
|
||||
<template v-else>
|
||||
<tr v-for="(profiles, username) in circleMentions">
|
||||
<tr v-for="(profiles, username) in circleMentions" v-if="username !== 'null'">
|
||||
<th>
|
||||
<LocaleLink :link="`/@${username}`" locale="_">@{{username}}</LocaleLink>
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li v-for="(relationship, locale) in profiles">
|
||||
<li v-for="(relationship, locale) in profiles" v-if="locale">
|
||||
<LocaleLink :link="`/@${username}`" :locale="locale">{{ locales[locale].name }}</LocaleLink><T>quotation.colon</T>
|
||||
{{ relationship }}
|
||||
</li>
|
||||
|
11
migrations/073-audit-log.sql
Normal file
11
migrations/073-audit-log.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- Up
|
||||
|
||||
CREATE TABLE audit_log (
|
||||
id TEXT not null primary key,
|
||||
userId TEXT,
|
||||
username TEXT,
|
||||
event TEXT not null,
|
||||
payload TEXT
|
||||
);
|
||||
|
||||
-- Down
|
13
server/audit.js
Normal file
13
server/audit.js
Normal file
@ -0,0 +1,13 @@
|
||||
import SQL from 'sql-template-strings';
|
||||
import {ulid} from "ulid";
|
||||
|
||||
export default async (req, event, payload = null) => {
|
||||
try {
|
||||
const user = req.user || req.rawUser || {id: null, username: null};
|
||||
await req.db.get(SQL`INSERT INTO audit_log (id, userId, username, event, payload) VALUES (
|
||||
${ulid()}, ${user.id}, ${user.username}, ${event}, ${payload ? JSON.stringify(payload) : null}
|
||||
)`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import {loadCurrentUser} from "./user";
|
||||
import {encodeTime, decodeTime, ulid} from "ulid";
|
||||
import Suml from 'suml';
|
||||
import buildLocaleList from "../../src/buildLocaleList";
|
||||
import auditLog from '../audit';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -335,11 +336,21 @@ router.post('/admin/propose-ban/:username', handleErrorAsync(async (req, res) =>
|
||||
${req.user.id}, ${req.body.terms.join(',')}, ${req.body.reason}
|
||||
)`
|
||||
);
|
||||
await auditLog(req, 'mod/proposed_ban', {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
terms: req.body.terms,
|
||||
reason: req.body.reason,
|
||||
});
|
||||
} else {
|
||||
await req.db.get(SQL`
|
||||
DELETE FROM ban_proposals
|
||||
WHERE userId = ${user.id} AND bannedBy = ${req.user.id}
|
||||
`);
|
||||
await auditLog(req, 'mod/cancelled_ban_proposal', {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
});
|
||||
}
|
||||
|
||||
return res.json(true);
|
||||
@ -375,6 +386,12 @@ router.post('/admin/apply-ban/:username/:id', handleErrorAsync(async (req, res)
|
||||
`);
|
||||
await archiveBan(req.db, user);
|
||||
mailer(user.email, 'ban', {reason: proposal.bannedReason, username: normalise(req.params.username)});
|
||||
await auditLog(req, 'mod/banned', {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
terms: proposal.bannedTerms,
|
||||
reason: proposal.bannedReason,
|
||||
});
|
||||
} else {
|
||||
await req.db.get(SQL`
|
||||
UPDATE users
|
||||
@ -382,6 +399,10 @@ router.post('/admin/apply-ban/:username/:id', handleErrorAsync(async (req, res)
|
||||
bannedBy = ${req.user.id}
|
||||
WHERE id = ${user.id}
|
||||
`);
|
||||
await auditLog(req, 'mod/unbanned', {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
});
|
||||
await liftBan(req.db, user);
|
||||
}
|
||||
|
||||
@ -440,6 +461,10 @@ router.post('/admin/reports/handle/:id', handleErrorAsync(async (req, res) => {
|
||||
WHERE id=${req.params.id}
|
||||
`);
|
||||
|
||||
await auditLog(req, 'mod/handled_report', {
|
||||
id: req.params.id,
|
||||
});
|
||||
|
||||
return res.json(true);
|
||||
}));
|
||||
|
||||
@ -480,6 +505,12 @@ router.post('/admin/mod-message/:username', handleErrorAsync(async (req, res) =>
|
||||
modUsername: req.user.username,
|
||||
});
|
||||
|
||||
await auditLog(req, 'mod/sent_mod_message', {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
message: req.body.message,
|
||||
});
|
||||
|
||||
return res.json(await fetchModMessages(req.db, user));
|
||||
}));
|
||||
|
||||
@ -522,6 +553,12 @@ router.post('/admin/overwrite-sensitive/:username', handleErrorAsync(async (req,
|
||||
});
|
||||
}
|
||||
|
||||
await auditLog(req, 'mod/overwrote_content_warnings', {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
warnings: req.body.sensitive,
|
||||
});
|
||||
|
||||
return res.json(req.body.sensitive);
|
||||
}));
|
||||
|
||||
@ -552,6 +589,10 @@ router.post('/admin/set-notification-frequency', handleErrorAsync(async (req, re
|
||||
|
||||
await req.db.get(SQL`UPDATE users SET adminNotifications = ${req.body.frequency} WHERE id = ${req.user.id}`);
|
||||
|
||||
await auditLog(req, 'team/changed_notification_frequency', {
|
||||
frequency: req.body.frequency,
|
||||
});
|
||||
|
||||
return await loadCurrentUser(req, res);
|
||||
}));
|
||||
|
||||
@ -570,7 +611,11 @@ router.post('/admin/timesheet', handleErrorAsync(async (req, res) => {
|
||||
return res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
await req.db.get(SQL`UPDATE users SET timesheets = ${JSON.stringify(req.body.timesheets)} WHERE id = ${req.user.id}`)
|
||||
await req.db.get(SQL`UPDATE users SET timesheets = ${JSON.stringify(req.body.timesheets)} WHERE id = ${req.user.id}`);
|
||||
|
||||
await auditLog(req, 'team/updated_timesheets', {
|
||||
timesheets: req.body.timesheets,
|
||||
});
|
||||
|
||||
return res.json('OK');
|
||||
}));
|
||||
|
@ -6,6 +6,7 @@ import Papa from 'papaparse';
|
||||
import {groupBy, handleErrorAsync, ImmutableArray} from "../../src/helpers";
|
||||
import {intersection, difference} from "../../src/sets";
|
||||
import {buildChart} from "../../src/stats";
|
||||
import auditLog from '../audit';
|
||||
|
||||
const getIp = req => {
|
||||
try {
|
||||
@ -92,6 +93,8 @@ router.post('/census/submit', handleErrorAsync(async (req, res) => {
|
||||
${boolToInt(isTroll(answers, writins))}
|
||||
)`);
|
||||
|
||||
await auditLog(req, 'census/submitted_answer');
|
||||
|
||||
return res.json(id);
|
||||
}));
|
||||
|
||||
@ -258,6 +261,11 @@ router.post('/census/moderation/decide', handleErrorAsync(async (req, res) => {
|
||||
UPDATE census SET troll = ${parseInt(req.body.decision)} WHERE id = ${req.body.id}
|
||||
`);
|
||||
|
||||
await auditLog(req, 'census/moderated_answer', {
|
||||
id: req.body.id,
|
||||
decision: req.body.decision,
|
||||
});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
|
@ -7,6 +7,7 @@ import sharp from 'sharp';
|
||||
import fs from 'fs';
|
||||
import SQL from 'sql-template-strings';
|
||||
import path from 'path';
|
||||
import auditLog from '../audit';
|
||||
|
||||
import awsConfig from '../aws';
|
||||
import S3 from 'aws-sdk/clients/s3';
|
||||
@ -91,6 +92,9 @@ router.post('/images/upload', multer({limits: {fileSize: 10 * 1024 * 1024}}).any
|
||||
|
||||
ids.push(id);
|
||||
}
|
||||
|
||||
await auditLog(req, 'images/uploaded', {ids});
|
||||
|
||||
return res.json(ids);
|
||||
}));
|
||||
|
||||
|
@ -3,6 +3,7 @@ import SQL from 'sql-template-strings';
|
||||
import {ulid} from "ulid";
|
||||
import {isTroll, handleErrorAsync, sortClearedLinkedText} from "../../src/helpers";
|
||||
import { caches } from "../../src/cache";
|
||||
import auditLog from '../audit';
|
||||
|
||||
const approve = async (db, id) => {
|
||||
const { base_id } = await db.get(SQL`SELECT base_id FROM inclusive WHERE id=${id}`);
|
||||
@ -67,9 +68,11 @@ router.post('/inclusive/submit', handleErrorAsync(async (req, res) => {
|
||||
${req.body.categories.join(',')}, ${JSON.stringify(req.body.links)}, ${req.body.clarification || null}
|
||||
)
|
||||
`);
|
||||
await auditLog(req, 'inclusive/submitted', {...req.body});
|
||||
|
||||
if (req.isGranted('inclusive')) {
|
||||
await approve(req.db, id);
|
||||
await auditLog(req, 'inclusive/approved', {id});
|
||||
}
|
||||
|
||||
return res.json('ok');
|
||||
@ -86,6 +89,8 @@ router.post('/inclusive/hide/:id', handleErrorAsync(async (req, res) => {
|
||||
WHERE id = ${req.params.id}
|
||||
`);
|
||||
|
||||
await auditLog(req, 'inclusive/hidden', {id: req.params.id});
|
||||
|
||||
await caches.inclusive.invalidate();
|
||||
|
||||
return res.json('ok');
|
||||
@ -98,6 +103,8 @@ router.post('/inclusive/approve/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await approve(req.db, req.params.id);
|
||||
|
||||
await auditLog(req, 'inclusive/approved', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -114,6 +121,8 @@ router.post('/inclusive/remove/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await caches.inclusive.invalidate();
|
||||
|
||||
await auditLog(req, 'inclusive/removed', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
fetchLoginAttempts, saveLoginAttempts
|
||||
} from './user';
|
||||
import cookieSettings from "../../src/cookieSettings";
|
||||
import auditLog from '../audit';
|
||||
|
||||
|
||||
export const addMfaInfo = async (db, user, guard = false) => {
|
||||
@ -75,11 +76,14 @@ router.post('/mfa/init', handleErrorAsync(async (req, res) => {
|
||||
|
||||
const token = await issueAuthentication(req.db, req.user);
|
||||
|
||||
await auditLog(req, 'auth/mfa_initialised');
|
||||
|
||||
return res.cookie('token', token, cookieSettings).json(recoveryCodes);
|
||||
}));
|
||||
|
||||
router.post('/mfa/validate', handleErrorAsync(async (req, res) => {
|
||||
if (!req.rawUser || !req.rawUser.mfaRequired) {
|
||||
await auditLog(req, 'auth/mfa_token_expired');
|
||||
return res.json({error: 'user.tokenExpired'});
|
||||
}
|
||||
|
||||
@ -90,10 +94,12 @@ router.post('/mfa/validate', handleErrorAsync(async (req, res) => {
|
||||
|
||||
const token = await issueAuthentication(req.db, req.rawUser, true, false, { mfa: false, mfaRequired: false });
|
||||
|
||||
await auditLog(req, 'auth/mfa_recovery_successful');
|
||||
return res.cookie('token', token, cookieSettings).json({token: token});
|
||||
}
|
||||
}
|
||||
|
||||
await auditLog(req, 'auth/mfa_recovery_failed');
|
||||
return res.json({error: 'user.code.invalid'});
|
||||
}
|
||||
|
||||
@ -101,6 +107,7 @@ router.post('/mfa/validate', handleErrorAsync(async (req, res) => {
|
||||
|
||||
const { attemptCount, firstAttemptAt, block } = await fetchLoginAttempts(req.db, authenticator.userId);
|
||||
if (block) {
|
||||
await auditLog(req, 'auth/mfa_too_many_attempts');
|
||||
return res.json({error: 'user.tooManyAttempts'});
|
||||
}
|
||||
|
||||
@ -118,11 +125,13 @@ router.post('/mfa/validate', handleErrorAsync(async (req, res) => {
|
||||
if (!tokenValidates) {
|
||||
await saveLoginAttempts(req.db, authenticator.userId, attemptCount + 1, firstAttemptAt || new Date());
|
||||
|
||||
await auditLog(req, 'auth/mfa_validation_failed');
|
||||
return res.json({error: 'user.code.invalid'});
|
||||
}
|
||||
|
||||
const token = await issueAuthentication(req.db, req.rawUser, true, false, { mfaRequired: false });
|
||||
|
||||
await auditLog(req, 'auth/mfa_validation_successful');
|
||||
return res.cookie('token', token, cookieSettings).json({token: token});
|
||||
}));
|
||||
|
||||
@ -135,6 +144,7 @@ router.post('/mfa/disable', handleErrorAsync(async (req, res) => {
|
||||
|
||||
const token = await issueAuthentication(req.db, req.user);
|
||||
|
||||
await auditLog(req, 'auth/mfa_disabled');
|
||||
return res.cookie('token', token, cookieSettings).json({token: token});
|
||||
}));
|
||||
|
||||
|
@ -3,6 +3,7 @@ import SQL from 'sql-template-strings';
|
||||
import {ulid} from "ulid";
|
||||
import {handleErrorAsync, isTroll} from "../../src/helpers";
|
||||
import { caches } from "../../src/cache";
|
||||
import auditLog from '../audit';
|
||||
|
||||
const approve = async (db, id) => {
|
||||
const { base_id } = await db.get(SQL`SELECT base_id FROM names WHERE id=${id}`);
|
||||
@ -58,9 +59,11 @@ router.post('/names/submit', handleErrorAsync(async (req, res) => {
|
||||
0, 0, ${req.body.base}, ${req.user ? req.user.id : null}
|
||||
)
|
||||
`);
|
||||
await auditLog(req, 'names/submitted', {...req.body});
|
||||
|
||||
if (req.isGranted('names')) {
|
||||
await approve(req.db, id);
|
||||
await auditLog(req, 'names/approved', {id});
|
||||
}
|
||||
|
||||
return res.json('ok');
|
||||
@ -79,6 +82,8 @@ router.post('/names/hide/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await caches.names.invalidate();
|
||||
|
||||
await auditLog(req, 'names/hidden', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -89,6 +94,8 @@ router.post('/names/approve/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await approve(req.db, req.params.id);
|
||||
|
||||
await auditLog(req, 'names/approved', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -105,6 +112,8 @@ router.post('/names/remove/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await caches.names.invalidate();
|
||||
|
||||
await auditLog(req, 'names/removed', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
|
@ -6,6 +6,7 @@ import {loadSuml} from "../loader";
|
||||
import {clearKey, handleErrorAsync, isTroll} from "../../src/helpers";
|
||||
import { caches } from "../../src/cache";
|
||||
import {registerLocaleFont} from "../localeFont";
|
||||
import auditLog from '../audit';
|
||||
|
||||
const translations = loadSuml('translations');
|
||||
|
||||
@ -117,9 +118,11 @@ router.post('/nouns/submit', handleErrorAsync(async (req, res) => {
|
||||
0, ${req.body.base}, ${global.config.locale}, ${req.user ? req.user.id : null}
|
||||
)
|
||||
`);
|
||||
await auditLog(req, 'nouns/submitted', {...req.body});
|
||||
|
||||
if (req.isGranted('nouns')) {
|
||||
await approve(req.db, id);
|
||||
await auditLog(req, 'nouns/approved', {id});
|
||||
}
|
||||
|
||||
return res.json('ok');
|
||||
@ -138,6 +141,8 @@ router.post('/nouns/hide/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await caches.nouns.invalidate();
|
||||
|
||||
await auditLog(req, 'nouns/hidden', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -148,6 +153,8 @@ router.post('/nouns/approve/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await approve(req.db, req.params.id);
|
||||
|
||||
await auditLog(req, 'nouns/approved', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -164,6 +171,8 @@ router.post('/nouns/remove/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await caches.nouns.invalidate();
|
||||
|
||||
await auditLog(req, 'nouns/removed', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
|
@ -12,6 +12,7 @@ import {downgradeToV1, upgradeToV2} from "../profileV2";
|
||||
import { colours, styles } from '../../src/styling';
|
||||
import {normaliseUrl} from "../../src/links";
|
||||
import allLocales from '../../locale/locales';
|
||||
import auditLog from '../audit';
|
||||
|
||||
const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // https://stackoverflow.com/a/6969486/3297012
|
||||
const normalise = s => decodeURIComponent(s.trim().toLowerCase());
|
||||
@ -527,6 +528,7 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
||||
}
|
||||
|
||||
if (req.body.username && req.user.username !== req.body.username) {
|
||||
await auditLog(req, 'profile/username_mismatch', {intended: req.body.username, token: req.user.username});
|
||||
return res.status(401).json({error: 'Payload username does not match the token'});
|
||||
}
|
||||
|
||||
@ -549,6 +551,7 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
||||
|| req.body.words.filter(c => c.values.length > 64).length > 0
|
||||
|| req.body.sensitive.length > 16
|
||||
) {
|
||||
await auditLog(req, 'profile/form_validation_failed', {...req.body});
|
||||
return res.status(400).json({error: 'crud.validation.genericForm'});
|
||||
}
|
||||
|
||||
@ -718,6 +721,7 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
||||
INSERT INTO reports (id, userId, reporterId, isAutomatic, comment, isHandled, snapshot)
|
||||
VALUES (${ulid()}, ${req.user.id}, null, 1, ${sus.join(', ')}, 0, ${await profilesSnapshot(req.db, normalise(req.user.username), new ProfileOptions({}))});
|
||||
`);
|
||||
await auditLog(req, 'profile/triggered_report', {...req.body});
|
||||
}
|
||||
|
||||
if (req.body.teamName) {
|
||||
@ -727,6 +731,8 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await req.db.get(SQL`UPDATE users SET inactiveWarning = null WHERE id = ${req.user.id}`);
|
||||
|
||||
await auditLog(req, 'profile/updated', {...req.body, locale: global.config.locale});
|
||||
|
||||
return res.json(await fetchProfiles(req.db, req.user.username, true));
|
||||
}));
|
||||
|
||||
@ -737,6 +743,8 @@ router.post('/profile/delete/:locale', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await req.db.get(SQL`DELETE FROM profiles WHERE userId = ${req.user.id} AND locale = ${req.params.locale}`);
|
||||
|
||||
await auditLog(req, 'profile/deleted', {locale: req.params.locale});
|
||||
|
||||
return res.json(await fetchProfiles(req.db, req.user.username, true));
|
||||
}));
|
||||
|
||||
@ -754,6 +762,12 @@ router.post('/profile/report/:username', handleErrorAsync(async (req, res) => {
|
||||
VALUES (${ulid()}, ${user.id}, ${req.user.id}, 0, ${req.body.comment}, 0, ${await profilesSnapshot(req.db, normalise(req.params.username))});
|
||||
`);
|
||||
|
||||
await auditLog(req, 'mod/reported', {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
comment: req.body.comment,
|
||||
});
|
||||
|
||||
return res.json('OK');
|
||||
}));
|
||||
|
||||
@ -776,6 +790,8 @@ router.post('/profile/request-card', handleErrorAsync(async (req, res) => {
|
||||
`);
|
||||
}
|
||||
|
||||
await auditLog(req, 'profile/requested_card_image');
|
||||
|
||||
return res.json('OK');
|
||||
}));
|
||||
|
||||
@ -808,6 +824,11 @@ router.post('/profile/remove-self-circle/:username', handleErrorAsync(async (req
|
||||
)
|
||||
`);
|
||||
|
||||
await auditLog(req, 'profile/removed_self_from_circle', {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
return res.json('OK');
|
||||
}));
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { Router } from 'express';
|
||||
import SQL from "sql-template-strings";
|
||||
import {ulid} from "ulid";
|
||||
import {clearKey, handleErrorAsync} from "../../src/helpers";
|
||||
import auditLog from '../audit';
|
||||
|
||||
const approve = async (db, id) => {
|
||||
const { base_id } = await db.get(SQL`SELECT base_id FROM sources WHERE id=${id}`);
|
||||
@ -98,9 +99,11 @@ router.post('/sources/submit', handleErrorAsync(async (req, res) => {
|
||||
${req.user ? req.user.id : null}, ${req.body.base}
|
||||
)
|
||||
`);
|
||||
await auditLog(req, 'sources/submitted', {...req.body});
|
||||
|
||||
if (req.isGranted('sources')) {
|
||||
await approve(req.db, id);
|
||||
await auditLog(req, 'sources/approved', {id});
|
||||
}
|
||||
|
||||
return res.json('ok');
|
||||
@ -117,6 +120,8 @@ router.post('/sources/hide/:id', handleErrorAsync(async (req, res) => {
|
||||
WHERE id = ${req.params.id}
|
||||
`);
|
||||
|
||||
await auditLog(req, 'sources/hidden', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -127,6 +132,8 @@ router.post('/sources/approve/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await approve(req.db, req.params.id);
|
||||
|
||||
await auditLog(req, 'sources/approved', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -141,6 +148,8 @@ router.post('/sources/remove/:id', handleErrorAsync(async (req, res) => {
|
||||
WHERE id = ${req.params.id}
|
||||
`);
|
||||
|
||||
await auditLog(req, 'sources/removed', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
|
@ -3,6 +3,7 @@ import SQL from 'sql-template-strings';
|
||||
import {ulid} from "ulid";
|
||||
import {handleErrorAsync} from "../../src/helpers";
|
||||
import {validateEmail} from "./user";
|
||||
import auditLog from '../audit';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -18,6 +19,11 @@ router.post('/subscription/subscribe', handleErrorAsync(async (req, res) => {
|
||||
if (existing.c === 0) {
|
||||
await req.db.get(SQL`INSERT INTO subscriptions (id, locale, type, email) VALUES
|
||||
(${ulid()}, ${global.config.locale}, ${req.body.type}, ${email})`)
|
||||
await auditLog(req, 'subscription/subscribed', {
|
||||
locale: global.config.locale,
|
||||
type: req.body.type,
|
||||
email,
|
||||
});
|
||||
}
|
||||
|
||||
return res.json('Subscribed');
|
||||
@ -31,6 +37,11 @@ router.get('/subscription/unsubscribe', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await req.db.get(SQL`DELETE FROM subscriptions WHERE email = ${req.query.email} AND type = ${req.query.type}`)
|
||||
|
||||
await auditLog(req, 'subscription/unsubscribed', {
|
||||
type: req.query.type,
|
||||
email: req.query.email,
|
||||
})
|
||||
|
||||
return res.json('Unsubscribed');
|
||||
}));
|
||||
|
||||
|
@ -3,6 +3,7 @@ import SQL from 'sql-template-strings';
|
||||
import {ulid} from "ulid";
|
||||
import {isTroll, handleErrorAsync, sortClearedLinkedText, clearKey} from "../../src/helpers";
|
||||
import { caches } from "../../src/cache";
|
||||
import auditLog from '../audit';
|
||||
|
||||
const approve = async (db, id) => {
|
||||
const { base_id } = await db.get(SQL`SELECT base_id FROM terms WHERE id=${id}`);
|
||||
@ -100,9 +101,11 @@ router.post('/terms/submit', handleErrorAsync(async (req, res) => {
|
||||
${req.body.categories.join(',')}, ${JSON.stringify(req.body.flags)}, ${req.body.images}
|
||||
)
|
||||
`);
|
||||
await auditLog(req, 'terms/submitted', {...req.body});
|
||||
|
||||
if (req.isGranted('terms')) {
|
||||
await approve(req.db, id);
|
||||
await auditLog(req, 'terms/approved', {id});
|
||||
}
|
||||
|
||||
return res.json('ok');
|
||||
@ -121,6 +124,8 @@ router.post('/terms/hide/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await caches.terms.invalidate();
|
||||
|
||||
await auditLog(req, 'terms/hidden', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -131,6 +136,8 @@ router.post('/terms/approve/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await approve(req.db, req.params.id);
|
||||
|
||||
await auditLog(req, 'terms/approved', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -147,6 +154,8 @@ router.post('/terms/remove/:id', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await caches.terms.invalidate();
|
||||
|
||||
await auditLog(req, 'terms/removed', {id: req.params.id});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
|
@ -4,6 +4,7 @@ import {ulid} from "ulid";
|
||||
import {findAdmins, handleErrorAsync} from "../../src/helpers";
|
||||
import mailer from "../../src/mailer";
|
||||
import {deduplicateEmailPreset} from "./user";
|
||||
import auditLog from '../audit';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -26,9 +27,15 @@ router.post('/translations/propose', handleErrorAsync(async (req, res) => {
|
||||
${ulid()}, ${global.config.locale},
|
||||
${tKey}, ${JSON.stringify(req.body.changes[tKey])},
|
||||
${req.isGranted('translations') ? TRANSLATION_STATUS.APPROVED : TRANSLATION_STATUS.AWAITING}, ${req.user.id}
|
||||
)`)
|
||||
)`);
|
||||
}
|
||||
|
||||
await auditLog(req, 'translations/proposed', {
|
||||
locale: global.config.locale,
|
||||
autoApproved: req.isGranted('translations'),
|
||||
changes: req.body.changes,
|
||||
});
|
||||
|
||||
if (req.isGranted('translations')) {
|
||||
for (let {email} of await findAdmins(req.db, global.config.locale, 'code')) {
|
||||
await deduplicateEmailPreset(req.db, email, 'translationToMerge', {locale: global.config.locale});
|
||||
@ -69,6 +76,11 @@ router.post('/translations/reject-proposal', handleErrorAsync(async (req, res) =
|
||||
|
||||
await req.db.get(SQL`UPDATE translations SET status = ${TRANSLATION_STATUS.REJECTED} WHERE id = ${req.body.id}`)
|
||||
|
||||
await auditLog(req, 'translations/rejected', {
|
||||
locale: global.config.locale,
|
||||
id: req.body.id,
|
||||
});
|
||||
|
||||
return res.json('OK');
|
||||
}));
|
||||
|
||||
@ -83,6 +95,11 @@ router.post('/translations/accept-proposal', handleErrorAsync(async (req, res) =
|
||||
await deduplicateEmailPreset(req.db, email, 'translationToMerge', {locale: global.config.locale});
|
||||
}
|
||||
|
||||
await auditLog(req, 'translations/accepted', {
|
||||
locale: global.config.locale,
|
||||
id: req.body.id,
|
||||
});
|
||||
|
||||
return res.json('OK');
|
||||
}));
|
||||
|
||||
@ -95,6 +112,11 @@ router.post('/translations/proposals-done', handleErrorAsync(async (req, res) =>
|
||||
await req.db.get(SQL`UPDATE translations SET status = ${TRANSLATION_STATUS.MERGED}
|
||||
WHERE locale = ${global.config.locale} AND status = ${TRANSLATION_STATUS.APPROVED}`)
|
||||
|
||||
await auditLog(req, 'translations/merged', {
|
||||
locale: global.config.locale,
|
||||
id: req.body.id,
|
||||
});
|
||||
|
||||
return res.json('OK');
|
||||
}));
|
||||
|
||||
|
@ -17,6 +17,7 @@ import copyAvatar from '../avatarCopy';
|
||||
import { usernameRegex, usernameUnsafeRegex } from '../../src/username';
|
||||
const config = loadSuml('config');
|
||||
const translations = loadSuml('translations');
|
||||
import auditLog from '../audit';
|
||||
|
||||
export const normalise = s => s.trim().toLowerCase();
|
||||
|
||||
@ -54,6 +55,15 @@ export const saveAuthenticator = async (db, type, user, payload, validForMinutes
|
||||
payload,
|
||||
});
|
||||
}
|
||||
await auditLog({db, user}, 'auth/saved_authenticator', {
|
||||
type,
|
||||
payloadId: payload.id || undefined,
|
||||
name: payload.name || undefined,
|
||||
username: payload.username || undefined,
|
||||
email: payload.email || undefined,
|
||||
from: payload.from || undefined,
|
||||
to: payload.to || undefined,
|
||||
})
|
||||
return id;
|
||||
}
|
||||
|
||||
@ -359,6 +369,7 @@ router.post('/user/init', handleErrorAsync(async (req, res) => {
|
||||
if (user) {
|
||||
const { block } = await fetchLoginAttempts(req.db, user.id);
|
||||
if (block) {
|
||||
await auditLog({...req, user}, 'auth/login_too_many_attempts');
|
||||
return res.json({error: 'user.tooManyAttempts'});
|
||||
}
|
||||
}
|
||||
@ -370,10 +381,12 @@ router.post('/user/init', handleErrorAsync(async (req, res) => {
|
||||
}
|
||||
|
||||
if (!await validateEmail(payload.email)) {
|
||||
await auditLog({...req, user}, 'auth/email_invalid', {email: payload.email});
|
||||
return res.json({ error: 'user.account.changeEmail.invalid' })
|
||||
}
|
||||
|
||||
if (!user && await lookupBanArchive(req.db, 'email', payload)) {
|
||||
await auditLog({...req, user}, 'auth/blocked_archive_ban');
|
||||
return res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
@ -388,10 +401,13 @@ router.post('/user/init', handleErrorAsync(async (req, res) => {
|
||||
codeKey = await saveAuthenticator(req.db, 'email', user, payload, 15);
|
||||
|
||||
mailer(payload.email, 'confirmCode', { code: payload.code });
|
||||
|
||||
await auditLog({...req, user}, 'auth/requested_email_code', {email: payload.email});
|
||||
},
|
||||
async () => {
|
||||
const auth = await findLatestEmailAuthenticator(req.db, payload.email, 'email');
|
||||
codeKey = auth ? auth.id : null;
|
||||
await auditLog({...req, user}, 'auth/requested_email_code_duplicate', {email: payload.email});
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -417,21 +433,26 @@ router.post('/user/validate', handleErrorAsync(async (req, res) => {
|
||||
|
||||
const authenticator = await findAuthenticatorById(req.db, req.rawUser.codeKey, 'email');
|
||||
if (!authenticator) {
|
||||
await auditLog(req, 'auth/validate_token_expired');
|
||||
return res.json({error: 'user.tokenExpired'});
|
||||
}
|
||||
|
||||
const { attemptCount, firstAttemptAt, block } = await fetchLoginAttempts(req.db, authenticator.userId);
|
||||
if (block) {
|
||||
await auditLog(req, 'auth/validate_too_many_attempts');
|
||||
return res.json({error: 'user.tooManyAttempts'});
|
||||
}
|
||||
|
||||
if (authenticator.payload.code !== normalise(req.body.code)) {
|
||||
await saveLoginAttempts(req.db, authenticator.userId, attemptCount + 1, firstAttemptAt || new Date());
|
||||
await auditLog(req, 'auth/validate_code_invalid');
|
||||
return res.json({error: 'user.code.invalid'});
|
||||
}
|
||||
|
||||
await invalidateAuthenticator(req.db, authenticator);
|
||||
|
||||
await auditLog(req, 'auth/validate_code_valid');
|
||||
|
||||
return res.json({token: await issueAuthentication(req.db, req.rawUser, true, true)});
|
||||
}));
|
||||
|
||||
@ -464,6 +485,7 @@ router.post('/user/change-email', handleErrorAsync(async (req, res) => {
|
||||
}
|
||||
|
||||
if (!await validateEmail(normalise(req.body.email))) {
|
||||
await auditLog(req, 'auth/email_invalid', {email: req.body.email});
|
||||
return res.json({ error: 'user.account.changeEmail.invalid' })
|
||||
}
|
||||
|
||||
@ -487,6 +509,11 @@ router.post('/user/change-email', handleErrorAsync(async (req, res) => {
|
||||
|
||||
mailer(payload.to, 'confirmCode', { code: payload.code });
|
||||
|
||||
await auditLog(req, 'auth/change_email_requested', {
|
||||
from: req.user.email,
|
||||
to: normalise(req.body.email)
|
||||
});
|
||||
|
||||
return res.json({ authId });
|
||||
}
|
||||
|
||||
@ -504,6 +531,11 @@ router.post('/user/change-email', handleErrorAsync(async (req, res) => {
|
||||
await req.db.get(SQL`UPDATE users SET email = ${authenticator.payload.to} WHERE id = ${req.user.id}`);
|
||||
req.user.email = authenticator.payload.to;
|
||||
|
||||
await auditLog(req, 'auth/changed_email', {
|
||||
from: authenticator.payload.from,
|
||||
to: authenticator.payload.to,
|
||||
});
|
||||
|
||||
return res.json({token: await issueAuthentication(req.db, req.user)});
|
||||
}));
|
||||
|
||||
@ -515,6 +547,8 @@ router.post('/user/delete', handleErrorAsync(async (req, res) => {
|
||||
await req.db.get('PRAGMA foreign_keys = ON');
|
||||
await req.db.get(SQL`DELETE FROM users WHERE id = ${req.user.id}`);
|
||||
|
||||
await auditLog(req, 'auth/removed_own_account');
|
||||
|
||||
return res.json(true);
|
||||
}));
|
||||
|
||||
@ -526,6 +560,8 @@ router.post('/user/data-erasure/:id', handleErrorAsync(async (req, res) => {
|
||||
await req.db.get('PRAGMA foreign_keys = ON');
|
||||
await req.db.get(SQL`DELETE FROM users WHERE id = ${req.params.id}`);
|
||||
|
||||
await auditLog(req, 'auth/removed_account_by_request', {userId: req.params.id});
|
||||
|
||||
return res.json(true);
|
||||
}));
|
||||
|
||||
@ -536,6 +572,8 @@ router.post('/user/:id/set-roles', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await req.db.get(SQL`UPDATE users SET roles = ${req.body.roles} WHERE id = ${req.params.id}`);
|
||||
|
||||
await auditLog(req, 'auth/granted_roles', {userId: req.params.id, roles: req.body.roles});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -649,6 +687,8 @@ router.post('/user/social-connection/:provider/disconnect', handleErrorAsync(asy
|
||||
AND provider = ${req.params.provider}
|
||||
`);
|
||||
|
||||
await auditLog(req, 'auth/disconnected', {type: req.params.provider});
|
||||
|
||||
return res.json('ok');
|
||||
}));
|
||||
|
||||
@ -665,6 +705,8 @@ router.post('/user/set-avatar', handleErrorAsync(async (req, res) => {
|
||||
|
||||
await resetCards(req.db, req.user.id);
|
||||
|
||||
await auditLog(req, 'auth/changed_avatar', {source: req.body.source});
|
||||
|
||||
return res.json({token: await issueAuthentication(req.db, req.user)});
|
||||
}));
|
||||
|
||||
@ -731,11 +773,13 @@ router.post('/user/set-social-lookup', handleErrorAsync(async (req, res) => {
|
||||
for (let authenticator of await findAuthenticatorsByUser(req.db, req.user)) {
|
||||
await saveAuthenticatorSocialLookup(req.db, authenticator);
|
||||
}
|
||||
await auditLog(req, 'auth/enabled_social_lookup');
|
||||
} else {
|
||||
await req.db.get(SQL`
|
||||
DELETE FROM social_lookup
|
||||
WHERE userId = ${req.user.id}
|
||||
`);
|
||||
await auditLog(req, 'auth/disabled_social_lookup');
|
||||
}
|
||||
|
||||
return res.json({token: await issueAuthentication(req.db, req.user)});
|
||||
|
Loading…
x
Reference in New Issue
Block a user