[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 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`
|
||||||
|
|
|
@ -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));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -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},
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
Reference in New Issue