[user] cleanup accounts
This commit is contained in:
parent
80f5a9b42f
commit
c10354c2f3
|
@ -0,0 +1,6 @@
|
|||
-- Up
|
||||
|
||||
ALTER TABLE users ADD COLUMN inactiveWarning INTEGER NULL;
|
||||
|
||||
-- Down
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
|
@ -9,6 +9,7 @@ import { caches } from "../../src/cache";
|
|||
import mailer from "../../src/mailer";
|
||||
import {profilesSnapshot} from "./profile";
|
||||
import buildLocaleList from "../../src/buildLocaleList";
|
||||
import {archiveBan, liftBan} from "../ban";
|
||||
|
||||
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))}
|
||||
WHERE id = ${user.id}
|
||||
`);
|
||||
await archiveBan(req.db, user);
|
||||
mailer(user.email, 'ban', {reason: req.body.reason});
|
||||
} else {
|
||||
await req.db.get(SQL`
|
||||
|
@ -168,6 +170,7 @@ router.post('/admin/ban/:username', handleErrorAsync(async (req, res) => {
|
|||
SET bannedReason = null
|
||||
WHERE id = ${user.id}
|
||||
`);
|
||||
await liftBan(req.db, user);
|
||||
}
|
||||
|
||||
await req.db.get(SQL`
|
||||
|
|
|
@ -273,6 +273,8 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
|||
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));
|
||||
}));
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import {validateCaptcha} from "../captcha";
|
|||
import assert from "assert";
|
||||
import {addMfaInfo} from './mfa';
|
||||
import buildLocaleList from "../../src/buildLocaleList";
|
||||
import {lookupBanArchive} from '../ban';
|
||||
|
||||
const config = loadSuml('config');
|
||||
const translations = loadSuml('translations');
|
||||
|
@ -35,6 +36,9 @@ const replaceExtension = username => username
|
|||
;
|
||||
|
||||
export const saveAuthenticator = async (db, type, user, payload, validForMinutes = null) => {
|
||||
if (await lookupBanArchive(db, type, payload)) {
|
||||
throw 'banned';
|
||||
}
|
||||
const id = ulid();
|
||||
await db.get(SQL`INSERT INTO authenticators (id, userId, type, payload, validUntil) VALUES (
|
||||
${id},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const config = {
|
||||
module.exports.config = {
|
||||
defaults: {
|
||||
origin: process.env.BASE_URL,
|
||||
transport: 'session',
|
||||
|
@ -34,7 +34,7 @@ export const config = {
|
|||
mastodon: {},
|
||||
}
|
||||
|
||||
export const handlers = {
|
||||
module.exports.handlers = {
|
||||
twitter(r) {
|
||||
return {
|
||||
id: r.profile.id_str,
|
||||
|
|
|
@ -74,6 +74,18 @@ const templates = {
|
|||
<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) => {
|
||||
|
|
Reference in New Issue