This repository has been archived on 2024-07-22. You can view files and clone it, but cannot push or open issues or pull requests.
Zaimki/server/routes/profile.js

339 lines
12 KiB
JavaScript
Raw Normal View History

2020-10-31 13:33:59 -07:00
import { Router } from 'express';
import SQL from 'sql-template-strings';
import md5 from "js-md5";
import {ulid} from "ulid";
2020-11-02 12:12:15 -08:00
import avatar from "../avatar";
2021-12-14 06:15:27 -08:00
import {handleErrorAsync, now} from "../../src/helpers";
2021-07-02 16:15:44 -07:00
import { caches } from "../../src/cache";
2021-11-23 05:30:51 -08:00
import fs from 'fs';
import { minBirthdate, maxBirthdate, formatDate, parseDate } from '../../src/birthdate';
2021-12-14 06:15:27 -08:00
import {socialProviders} from "../../src/socialProviders";
2020-10-31 13:33:59 -07:00
2021-01-01 12:11:37 -08:00
const normalise = s => s.trim().toLowerCase();
2020-10-31 13:33:59 -07:00
const calcAge = birthday => {
if (!birthday) {
return null;
}
const now = new Date();
const birth = parseDate(birthday);
2020-10-31 13:33:59 -07:00
const diff = now.getTime() - birth.getTime();
return parseInt(Math.floor(diff / 1000 / 60 / 60 / 24 / 365.25));
}
2021-12-14 06:15:27 -08:00
const providersWithLinks = Object.keys(socialProviders)
.filter(p => socialProviders[p].linkRegex !== undefined);
const verifyLinks = (links, authenticators) => {
const verifiedLinks = {};
for (let provider of providersWithLinks) {
for (let a of authenticators) {
if (a.type !== provider) { continue; }
const regex = new RegExp(socialProviders[a.type].linkRegex(JSON.parse(a.payload)));
for (let link of links) {
if (link.match(regex)) {
verifiedLinks[link] = provider;
}
}
}
}
return verifiedLinks;
}
const fetchProfiles = async (db, username, self) => {
2020-10-31 13:33:59 -07:00
const profiles = await db.all(SQL`
2021-12-14 06:15:27 -08:00
SELECT profiles.*
FROM profiles
LEFT JOIN users on users.id == profiles.userId
2021-07-14 06:28:53 -07:00
WHERE usernameNorm = ${normalise(username)}
2020-10-31 13:33:59 -07:00
ORDER BY profiles.locale
`);
2021-12-14 06:15:27 -08:00
const linkAuthenticators = await db.all(SQL`
SELECT a.type, a.payload
FROM authenticators a
LEFT JOIN users u on u.id == a.userId
WHERE u.usernameNorm = ${normalise(username)}
AND a.type IN (`.append(providersWithLinks.map(k => `'${k}'`).join(',')).append(SQL`)
AND (a.validUntil IS NULL OR a.validUntil > ${now()})
`));
2020-11-02 12:12:15 -08:00
const p = {}
for (let profile of profiles) {
2021-12-14 06:15:27 -08:00
const links = JSON.parse(profile.links);
2020-11-02 12:12:15 -08:00
p[profile.locale] = {
names: JSON.parse(profile.names),
pronouns: JSON.parse(profile.pronouns),
description: profile.description,
age: calcAge(profile.birthday),
2021-12-14 06:15:27 -08:00
links: links,
verifiedLinks: verifyLinks(links, linkAuthenticators),
2020-11-02 12:12:15 -08:00
flags: JSON.parse(profile.flags),
2021-04-05 08:03:13 -07:00
customFlags: JSON.parse(profile.customFlags),
2020-11-02 12:12:15 -08:00
words: JSON.parse(profile.words),
birthday: self ? profile.birthday : undefined,
2021-01-12 11:06:59 -08:00
teamName: profile.teamName,
footerName: profile.footerName,
footerAreas: profile.footerAreas ? profile.footerAreas.split(',') : [],
2021-08-26 05:25:59 -07:00
credentials: profile.credentials ? profile.credentials.split('|') : [],
credentialsLevel: profile.credentialsLevel,
credentialsName: profile.credentialsName,
2021-07-10 07:46:29 -07:00
card: profile.card,
cardDark: profile.cardDark,
2020-11-02 12:12:15 -08:00
};
}
return p;
2020-10-31 13:33:59 -07:00
};
export const profilesSnapshot = async (db, username) => {
return JSON.stringify(await fetchProfiles(db, username, true), null, 4);
}
2021-11-23 05:30:51 -08:00
const susRegexes = fs.readFileSync(__dirname + '/../../sus.txt').toString('utf-8').split('\n').filter(x => !!x);
2021-07-24 10:18:39 -07:00
2021-11-23 05:30:51 -08:00
function* isSuspicious(profile) {
2021-12-12 03:09:57 -08:00
for (let s of [
profile.description,
JSON.stringify(profile.customFlags),
JSON.stringify(profile.pronouns),
JSON.stringify(profile.names),
JSON.stringify(profile.words),
]) {
2021-11-23 05:30:51 -08:00
s = s.toLowerCase().replace(/\s+/g, ' ');
for (let sus of susRegexes) {
let m = s.match(new RegExp(sus, 'ig'));
if (m) {
yield `${m[0]} (${sus})`;
}
}
}
2021-07-24 10:18:39 -07:00
}
const hasAutomatedReports = async (db, id) => {
return (await db.get(SQL`SELECT COUNT(*) AS c FROM reports WHERE userId = ${id} AND isAutomatic = 1`)).c > 0;
2021-07-24 10:18:39 -07:00
}
2020-10-31 13:33:59 -07:00
const router = Router();
router.get('/profile/get/:username', handleErrorAsync(async (req, res) => {
const isSelf = req.user && req.user.username === req.params.username;
const isAdmin = req.isGranted('users');
const user = await req.db.get(SQL`
SELECT
users.id,
users.username,
users.email,
users.avatarSource,
users.bannedReason,
2021-08-12 03:14:34 -07:00
users.bannedTerms,
users.roles != '' AS team
FROM users
WHERE users.usernameNorm = ${normalise(req.params.username)}
`);
if (!user || (user.bannedReason !== null && !isAdmin && !isSelf)) {
return res.json({
profiles: {},
});
}
user.emailHash = md5(user.email);
delete user.email;
user.avatar = await avatar(req.db, user);
2021-08-12 03:14:34 -07:00
user.bannedTerms = user.bannedTerms ? user.bannedTerms.split(',') : [];
return res.json({
...user,
profiles: await fetchProfiles(req.db, req.params.username, isSelf),
});
}));
2020-10-31 13:33:59 -07:00
const sanitiseBirthday = (bd) => {
if (!bd) { return null; }
const match = bd.match(/^(\d\d\d\d)-(\d\d)-(\d\d)$/);
if (!match) { return null; }
bd = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]));
if (bd < minBirthdate || bd > maxBirthdate) {
return null;
}
return formatDate(bd);
}
router.post('/profile/save', handleErrorAsync(async (req, res) => {
2020-10-31 13:33:59 -07:00
if (!req.user) {
return res.status(401).json({error: 'Unauthorised'});
}
2021-06-03 01:28:53 -07:00
// TODO just make it a transaction...
const ids = (await req.db.all(SQL`SELECT * FROM profiles WHERE userId = ${req.user.id} AND locale = ${global.config.locale}`)).map(row => row.id);
2021-06-03 01:28:53 -07:00
if (ids.length) {
await req.db.get(SQL`UPDATE profiles
SET
names = ${JSON.stringify(req.body.names)},
pronouns = ${JSON.stringify(req.body.pronouns)},
description = ${req.body.description},
birthday = ${sanitiseBirthday(req.body.birthday || null)},
2021-06-03 01:28:53 -07:00
links = ${JSON.stringify(req.body.links.filter(x => !!x))},
flags = ${JSON.stringify(req.body.flags)},
customFlags = ${JSON.stringify(req.body.customFlags)},
words = ${JSON.stringify(req.body.words)},
2021-08-11 03:23:29 -07:00
teamName = ${req.isGranted() ? req.body.teamName || null : ''},
footerName = ${req.isGranted() ? req.body.footerName || null : ''},
footerAreas = ${req.isGranted() ? req.body.footerAreas.join(',') || null : ''},
2021-08-26 05:25:59 -07:00
credentials = ${req.isGranted() ? req.body.credentials.join('|') || null : null},
credentialsLevel = ${req.isGranted() ? req.body.credentialsLevel || null : null},
credentialsName = ${req.isGranted() ? req.body.credentialsName || null : null},
card = NULL,
cardDark = NULL
2021-06-03 01:28:53 -07:00
WHERE id = ${ids[0]}
`);
} else {
await req.db.get(SQL`INSERT INTO profiles (id, userId, locale, names, pronouns, description, birthday, links, flags, customFlags, words, active, teamName, footerName, footerAreas)
VALUES (${ulid()}, ${req.user.id}, ${global.config.locale}, ${JSON.stringify(req.body.names)}, ${JSON.stringify(req.body.pronouns)},
${req.body.description}, ${sanitiseBirthday(req.body.birthday || null)}, ${JSON.stringify(req.body.links.filter(x => !!x))}, ${JSON.stringify(req.body.flags)}, ${JSON.stringify(req.body.customFlags)},
2021-01-12 11:06:59 -08:00
${JSON.stringify(req.body.words)}, 1,
2021-08-11 03:23:29 -07:00
${req.isGranted() ? req.body.teamName || null : ''},
${req.isGranted() ? req.body.footerName || null : ''},
${req.isGranted() ? req.body.footerAreas.join(',') || null : ''}
2021-06-03 01:28:53 -07:00
)`);
}
2020-10-31 13:33:59 -07:00
if ((req.body.propagate || []).includes('teamName')) {
await req.db.get(SQL`UPDATE profiles
SET teamName = ${req.isGranted() ? req.body.teamName || null : ''}
WHERE userId = ${req.user.id} AND teamName != '' AND teamName IS NOT NULL;
`);
}
if ((req.body.propagate || []).includes('footerName')) {
await req.db.get(SQL`UPDATE profiles
SET footerName = ${req.isGranted() ? req.body.footerName || null : ''}
WHERE userId = ${req.user.id} AND footerName != '' AND footerName IS NOT NULL;
`);
}
if ((req.body.propagate || []).includes('names')) {
await req.db.get(SQL`UPDATE profiles
SET names = ${JSON.stringify(req.body.names)}
WHERE userId = ${req.user.id};
`);
}
if ((req.body.propagate || []).includes('flags')) {
await req.db.get(SQL`UPDATE profiles
SET flags = ${JSON.stringify(req.body.flags)}
WHERE userId = ${req.user.id};
`);
}
if ((req.body.propagate || []).includes('customFlags')) {
await req.db.get(SQL`UPDATE profiles
customFlags = ${JSON.stringify(req.body.customFlags)}
WHERE userId = ${req.user.id};
`);
}
if ((req.body.propagate || []).includes('links')) {
await req.db.get(SQL`UPDATE profiles
SET links = ${JSON.stringify(req.body.links.filter(x => !!x))}
WHERE userId = ${req.user.id};
`);
}
if ((req.body.propagate || []).includes('links')) {
await req.db.get(SQL`UPDATE profiles
SET links = ${JSON.stringify(req.body.links.filter(x => !!x))}
WHERE userId = ${req.user.id};
`);
}
if ((req.body.propagate || []).includes('birthday')) {
await req.db.get(SQL`UPDATE profiles
SET birthday = ${sanitiseBirthday(req.body.birthday || null)}
WHERE userId = ${req.user.id};
`);
}
2021-07-24 10:18:39 -07:00
const sus = [...isSuspicious(req.body)];
if (sus.length && !await hasAutomatedReports(req.db, req.user.id)) {
2021-07-24 10:18:39 -07:00
await req.db.get(SQL`
INSERT INTO reports (id, userId, reporterId, isAutomatic, comment, isHandled, snapshot)
2021-12-22 11:54:17 -08:00
VALUES (${ulid()}, ${req.user.id}, null, 1, ${sus.join(', ')}, 0, ${await profilesSnapshot(req.db, normalise(req.user.username))});
2021-07-24 10:18:39 -07:00
`);
}
2021-07-02 16:15:44 -07:00
if (req.body.teamName) {
await caches.admins.invalidate();
await caches.adminsFooter.invalidate();
}
2020-10-31 13:33:59 -07:00
return res.json(await fetchProfiles(req.db, req.user.username, true));
}));
2020-10-31 13:33:59 -07:00
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}`);
2020-10-31 13:33:59 -07:00
return res.json(await fetchProfiles(req.db, req.user.username, true));
}));
2020-10-31 13:33:59 -07:00
2021-07-24 10:18:39 -07:00
router.post('/profile/report/:username', handleErrorAsync(async (req, res) => {
const user = await req.db.get(SQL`SELECT id FROM users WHERE usernameNorm = ${normalise(req.params.username)}`);
if (!user) {
return res.status(400).json({error: 'Missing user'});
}
if (!req.body.comment) {
return res.status(400).json({error: 'Missing comment'});
}
await req.db.get(SQL`
INSERT INTO reports (id, userId, reporterId, isAutomatic, comment, isHandled, snapshot)
VALUES (${ulid()}, ${user.id}, ${req.user.id}, 0, ${req.body.comment}, 0, ${await profilesSnapshot(req.db, normalise(req.params.username))});
2021-07-24 10:18:39 -07:00
`);
return res.json('OK');
}));
router.post('/profile/request-card', handleErrorAsync(async (req, res) => {
if (!req.user) {
return res.status(400).json({error: 'Missing user'});
}
if (req.query.dark === '1') {
await req.db.get(SQL`
UPDATE profiles
SET cardDark = ''
WHERE userId=${req.user.id} AND locale=${global.config.locale} AND cardDark IS NULL
`);
} else {
await req.db.get(SQL`
UPDATE profiles
SET card = ''
WHERE userId=${req.user.id} AND locale=${global.config.locale} AND card IS NULL
`);
}
return res.json('OK');
}));
router.get('/profile/has-card', handleErrorAsync(async (req, res) => {
if (!req.user) {
return res.status(400).json({error: 'Missing user'});
}
const card = await req.db.get(SQL`
SELECT card, cardDark
FROM profiles
WHERE userId=${req.user.id} AND locale=${global.config.locale}
`);
return res.json(card);
}));
2020-10-31 13:33:59 -07:00
export default router;