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

import { Router } from 'express';
import SQL from 'sql-template-strings';
import md5 from "js-md5";
import {ulid} from "ulid";
import avatar from "../avatar";
import {handleErrorAsync, now} from "../../src/helpers";
import { caches } from "../../src/cache";
import fs from 'fs';
import { minBirthdate, maxBirthdate, formatDate, parseDate } from '../../src/birthdate';
import {socialProviders} from "../../src/socialProviders";
const normalise = s => s.trim().toLowerCase();
const calcAge = birthday => {
if (!birthday) {
return null;
}
const now = new Date();
const birth = parseDate(birthday);
const diff = now.getTime() - birth.getTime();
return parseInt(Math.floor(diff / 1000 / 60 / 60 / 24 / 365.25));
}
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) => {
const profiles = await db.all(SQL`
SELECT profiles.*
FROM profiles
LEFT JOIN users on users.id == profiles.userId
WHERE usernameNorm = ${normalise(username)}
ORDER BY profiles.locale
`);
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()})
`));
const p = {}
for (let profile of profiles) {
const links = JSON.parse(profile.links);
p[profile.locale] = {
names: JSON.parse(profile.names),
pronouns: JSON.parse(profile.pronouns),
description: profile.description,
age: calcAge(profile.birthday),
links: links,
verifiedLinks: verifyLinks(links, linkAuthenticators),
flags: JSON.parse(profile.flags),
customFlags: JSON.parse(profile.customFlags),
words: JSON.parse(profile.words),
birthday: self ? profile.birthday : undefined,
teamName: profile.teamName,
footerName: profile.footerName,
footerAreas: profile.footerAreas ? profile.footerAreas.split(',') : [],
credentials: profile.credentials ? profile.credentials.split('|') : [],
credentialsLevel: profile.credentialsLevel,
credentialsName: profile.credentialsName,
card: profile.card,
cardDark: profile.cardDark,
};
}
return p;
};
export const profilesSnapshot = async (db, username) => {
return JSON.stringify(await fetchProfiles(db, username, true), null, 4);
}
const susRegexes = fs.readFileSync(__dirname + '/../../sus.txt').toString('utf-8').split('\n').filter(x => !!x);
function* isSuspicious(profile) {
for (let s of [
profile.description,
JSON.stringify(profile.customFlags),
JSON.stringify(profile.pronouns),
JSON.stringify(profile.names),
JSON.stringify(profile.words),
]) {
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})`;
}
}
}
}
const hasAutomatedReports = async (db, id) => {
return (await db.get(SQL`SELECT COUNT(*) AS c FROM reports WHERE userId = ${id} AND isAutomatic = 1`)).c > 0;
}
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,
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);
user.bannedTerms = user.bannedTerms ? user.bannedTerms.split(',') : [];
return res.json({
...user,
profiles: await fetchProfiles(req.db, req.params.username, isSelf),
});
}));
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) => {
if (!req.user) {
return res.status(401).json({error: 'Unauthorised'});
}
// 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);
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)},
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)},
teamName = ${req.isGranted() ? req.body.teamName || null : ''},
footerName = ${req.isGranted() ? req.body.footerName || null : ''},
footerAreas = ${req.isGranted() ? req.body.footerAreas.join(',') || null : ''},
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
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)},
${JSON.stringify(req.body.words)}, 1,
${req.isGranted() ? req.body.teamName || null : ''},
${req.isGranted() ? req.body.footerName || null : ''},
${req.isGranted() ? req.body.footerAreas.join(',') || null : ''}
)`);
}
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};
`);
}
const sus = [...isSuspicious(req.body)];
if (sus.length && !await hasAutomatedReports(req.db, req.user.id)) {
await req.db.get(SQL`
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.params.username))});
`);
}
if (req.body.teamName) {
await caches.admins.invalidate();
await caches.adminsFooter.invalidate();
}
return res.json(await fetchProfiles(req.db, req.user.username, true));
}));
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}`);
return res.json(await fetchProfiles(req.db, req.user.username, true));
}));
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))});
`);
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);
}));
export default router;