audit log

This commit is contained in:
Andrea Vos 2023-07-27 21:37:36 +02:00
parent 7ac68dcd1f
commit cf5a868f78
16 changed files with 238 additions and 4 deletions

View File

@ -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>

View 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
View 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);
}
}

View File

@ -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');
}));

View File

@ -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');
}));

View File

@ -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);
}));

View File

@ -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');
}));

View File

@ -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});
}));

View File

@ -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');
}));

View File

@ -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');
}));

View File

@ -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');
}));

View File

@ -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');
}));

View File

@ -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');
}));

View File

@ -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');
}));

View File

@ -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');
}));

View File

@ -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)});