[user] cleanup accounts

This commit is contained in:
Andrea 2022-02-12 18:28:56 +01:00
parent 80f5a9b42f
commit c10354c2f3
9 changed files with 201 additions and 2 deletions

View File

@ -0,0 +1,6 @@
-- Up
ALTER TABLE users ADD COLUMN inactiveWarning INTEGER NULL;
-- Down

View File

@ -0,0 +1,12 @@
-- Up
CREATE TABLE bans (
type TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (type, value)
);
INSERT INTO bans VALUES ('email', 'myriamnicoleacostarodriguez@gmail.com');
INSERT INTO bans VALUES ('email', 'beherit1015@gmail.com');
-- Down

55
server/ban.js Normal file
View File

@ -0,0 +1,55 @@
const SQL = require('sql-template-strings');
const socialLoginConfig = require('./social').config;
const upsertBanArchive = async (db, type, value) => {
await db.get(SQL`INSERT INTO bans (type, value) VALUES (${type}, ${value}) ON CONFLICT DO NOTHING`);
}
const removeBanArchive = async (db, type, value) => {
await db.get(SQL`DELETE FROM bans WHERE type = ${type} AND value = ${value});`);
}
const normaliseEmail = (email) => {
let [username, domain] = email.split('@');
username = username.replace(/\.+/g, '');
username = username.replace(/\+.*/, '');
return `${username}@${domain}`.toLowerCase();
};
module.exports.archiveBan = async (db, user) => {
for (let auth of await db.all(SQL`SELECT * FROM authenticators WHERE userId = ${user.id}`)) {
const p = JSON.parse(auth.payload);
if (auth.type === 'email') {
await upsertBanArchive(db, 'email', normaliseEmail(p.email));
} else if (socialLoginConfig[auth.type] !== undefined) {
await upsertBanArchive(db, auth.type, p.id);
if (p.email) {
await upsertBanArchive(db, 'email', normaliseEmail(p.email));
}
}
}
}
module.exports.liftBan = async (db, user) => {
for (let auth of await db.all(SQL`SELECT * FROM authenticators WHERE userId = ${user.id}`)) {
const p = JSON.parse(auth.payload);
if (auth.type === 'email') {
await removeBanArchive(db, 'email', normaliseEmail(p.email));
} else if (socialLoginConfig[auth.type] !== undefined) {
await removeBanArchive(db, auth.type, p.id);
if (p.email) {
await removeBanArchive(db, 'email', normaliseEmail(p.email));
}
}
}
}
module.exports.lookupBanArchive = async (db, type, value) => {
if (type === 'email') {
value = normaliseEmail(value.email);
} else {
value = value.id;
}
return (await db.all(SQL`SELECT * FROM bans WHERE type=${type} and value = ${value}`)).length > 0;
}

105
server/cleanupAccounts.js Normal file
View File

@ -0,0 +1,105 @@
require('../src/dotenv')();
const dbConnection = require('./db');
const mailer = require('../src/mailer');
const {archiveBan} = require("./ban");
const execute = process.env.EXECUTE === '1';
console.log(execute ? 'WILL EXECUTE!' : 'Dry run');
const now = +new Date();
const month = 30*24*60*60*1000;
const week = 7*24*60*60*1000;
const sleep = ms => new Promise(res => setTimeout(res, ms));
async function warnInactive(db) {
console.log('--- Fetching ids to warn ---');
const users = (await db.all(`
SELECT u.id, u.username, u.email, u.bannedReason
FROM users u
WHERE
inactiveWarning IS NULL
AND (
(
u.id NOT IN (SELECT DISTINCT p.userId FROM profiles p)
AND (lastActive IS NULL OR lastActive < ${now - month})
)
OR bannedReason IS NOT NULL
)
`));
console.log(users.length);
for (let user of users) {
console.log('warn', user);
if (!execute) { continue; }
if (user.email.endsWith('.oauth')) {
await db.get(`UPDATE users SET inactiveWarning = ${now - week - 1000} WHERE id = '${user.id}'`);
continue;
}
await db.get(`UPDATE users SET inactiveWarning = ${now} WHERE id = '${user.id}'`);
if (user.bannedReason !== null) {
continue;
}
mailer(user.email, 'inactivityWarning')
await sleep(3000);
}
}
async function removeWarned(db) {
console.log('--- Fetching ids to remove ---');
const users = (await db.all(`
SELECT u.id, u.username, u.email
FROM users u
WHERE (
u.id NOT IN (
SELECT DISTINCT p.userId
FROM profiles p
)
OR bannedReason IS NOT NULL
)
AND inactiveWarning IS NOT NULL
AND inactiveWarning < ${now - week}
`));
console.log(users.length);
for (let user of users) {
console.log('remove', user);
if (!execute) { continue; }
await db.get(`DELETE FROM users WHERE id = '${user.id}'`);
}
}
async function archiveBans(db) {
console.log('--- Archiving banned accounts ---');
const users = (await db.all(`
SELECT u.id, u.username, u.email
FROM users u
WHERE u.bannedReason IS NOT NULL
`));
console.log(users.length);
for (let user of users) {
console.log('archiveBan', user);
if (!execute) { continue; }
await archiveBan(db, user);
}
}
async function cleanup() {
const db = await dbConnection();
await db.get('PRAGMA foreign_keys = ON')
await archiveBans(db); // TODO one-time only
await warnInactive(db);
await removeWarned(db);
}
cleanup();

View File

@ -9,6 +9,7 @@ import { caches } from "../../src/cache";
import mailer from "../../src/mailer"; import mailer from "../../src/mailer";
import {profilesSnapshot} from "./profile"; import {profilesSnapshot} from "./profile";
import buildLocaleList from "../../src/buildLocaleList"; import buildLocaleList from "../../src/buildLocaleList";
import {archiveBan, liftBan} from "../ban";
const router = Router(); const router = Router();
@ -161,6 +162,7 @@ router.post('/admin/ban/:username', handleErrorAsync(async (req, res) => {
banSnapshot = ${await profilesSnapshot(req.db, normalise(req.params.username))} banSnapshot = ${await profilesSnapshot(req.db, normalise(req.params.username))}
WHERE id = ${user.id} WHERE id = ${user.id}
`); `);
await archiveBan(req.db, user);
mailer(user.email, 'ban', {reason: req.body.reason}); mailer(user.email, 'ban', {reason: req.body.reason});
} else { } else {
await req.db.get(SQL` await req.db.get(SQL`
@ -168,6 +170,7 @@ router.post('/admin/ban/:username', handleErrorAsync(async (req, res) => {
SET bannedReason = null SET bannedReason = null
WHERE id = ${user.id} WHERE id = ${user.id}
`); `);
await liftBan(req.db, user);
} }
await req.db.get(SQL` await req.db.get(SQL`

View File

@ -273,6 +273,8 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
await caches.adminsFooter.invalidate(); await caches.adminsFooter.invalidate();
} }
await req.db.get(SQL`UPDATE users SET inactiveWarning = null WHERE id = ${req.user.id}`);
return res.json(await fetchProfiles(req.db, req.user.username, true)); return res.json(await fetchProfiles(req.db, req.user.username, true));
})); }));

View File

@ -12,6 +12,7 @@ import {validateCaptcha} from "../captcha";
import assert from "assert"; import assert from "assert";
import {addMfaInfo} from './mfa'; import {addMfaInfo} from './mfa';
import buildLocaleList from "../../src/buildLocaleList"; import buildLocaleList from "../../src/buildLocaleList";
import {lookupBanArchive} from '../ban';
const config = loadSuml('config'); const config = loadSuml('config');
const translations = loadSuml('translations'); const translations = loadSuml('translations');
@ -35,6 +36,9 @@ const replaceExtension = username => username
; ;
export const saveAuthenticator = async (db, type, user, payload, validForMinutes = null) => { export const saveAuthenticator = async (db, type, user, payload, validForMinutes = null) => {
if (await lookupBanArchive(db, type, payload)) {
throw 'banned';
}
const id = ulid(); const id = ulid();
await db.get(SQL`INSERT INTO authenticators (id, userId, type, payload, validUntil) VALUES ( await db.get(SQL`INSERT INTO authenticators (id, userId, type, payload, validUntil) VALUES (
${id}, ${id},

View File

@ -1,4 +1,4 @@
export const config = { module.exports.config = {
defaults: { defaults: {
origin: process.env.BASE_URL, origin: process.env.BASE_URL,
transport: 'session', transport: 'session',
@ -34,7 +34,7 @@ export const config = {
mastodon: {}, mastodon: {},
} }
export const handlers = { module.exports.handlers = {
twitter(r) { twitter(r) {
return { return {
id: r.profile.id_str, id: r.profile.id_str,

View File

@ -74,6 +74,18 @@ const templates = {
<p style="font-size: 12px; color: #777">[[quotation.start]]${terms}[[quotation.end]]</p> <p style="font-size: 12px; color: #777">[[quotation.start]]${terms}[[quotation.end]]</p>
`, `,
}, },
inactivityWarning: {
subject: '[[user.removeInactive.email.subject]]',
text: '[[user.removeInactive.email.content]]',
html: `
<p>[[user.removeInactive.email.content]]</p>
<p style="text-align: center; padding-top: 16px; padding-bottom: 16px;">
<a href="https://en.pronouns.page/account" target="_blank" rel="noopener" style="background-color: #C71585; color: #fff; padding: 8px 16px; border: none; border-radius: 6px;text-decoration: none">
[[user.removeInactive.email.cta]]
</a>
</p>
`,
},
} }
const applyTemplate = (template, context, params) => { const applyTemplate = (template, context, params) => {