2020-10-31 13:33:59 -07:00
|
|
|
import { Router } from 'express';
|
|
|
|
import SQL from 'sql-template-strings';
|
|
|
|
import {ulid} from "ulid";
|
2022-03-19 13:46:34 -07:00
|
|
|
import {buildDict, makeId, now, handleErrorAsync, obfuscateEmail} from "../../src/helpers";
|
2020-10-31 13:33:59 -07:00
|
|
|
import jwt from "../../src/jwt";
|
|
|
|
import mailer from "../../src/mailer";
|
2020-11-10 14:41:56 -08:00
|
|
|
import { loadSuml } from '../loader';
|
2020-11-02 12:12:15 -08:00
|
|
|
import avatar from '../avatar';
|
2020-11-02 12:45:45 -08:00
|
|
|
import { config as socialLoginConfig, handlers as socialLoginHandlers } from '../social';
|
2021-02-04 04:11:47 -08:00
|
|
|
import cookieSettings from "../../src/cookieSettings";
|
2021-08-07 03:03:49 -07:00
|
|
|
import {validateCaptcha} from "../captcha";
|
2021-12-14 15:45:28 -08:00
|
|
|
import assert from "assert";
|
2021-12-18 10:54:36 -08:00
|
|
|
import {addMfaInfo} from './mfa';
|
2022-01-03 10:22:10 -08:00
|
|
|
import buildLocaleList from "../../src/buildLocaleList";
|
2022-02-12 09:28:56 -08:00
|
|
|
import {lookupBanArchive} from '../ban';
|
2020-10-31 13:33:59 -07:00
|
|
|
|
2020-11-10 14:41:56 -08:00
|
|
|
const config = loadSuml('config');
|
|
|
|
const translations = loadSuml('translations');
|
|
|
|
|
2020-10-31 13:33:59 -07:00
|
|
|
const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';
|
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
export const normalise = s => s.trim().toLowerCase();
|
2020-10-31 13:33:59 -07:00
|
|
|
|
2021-08-07 02:14:53 -07:00
|
|
|
const isSpam = (email) => {
|
2021-08-07 02:40:03 -07:00
|
|
|
const noDots = email.replace(/\./g, '');
|
|
|
|
return noDots === 'javierfranciscotmp@gmailcom'
|
2021-12-18 13:37:26 -08:00
|
|
|
|| noDots === 'leahmarykathryntmp@gmailcom'
|
2021-08-07 02:52:23 -07:00
|
|
|
|| email.includes('dogazu')
|
|
|
|
|| email.includes('narodowcy.net')
|
2021-08-07 02:14:53 -07:00
|
|
|
|| email.length > 128;
|
|
|
|
}
|
|
|
|
|
2021-12-07 03:28:36 -08:00
|
|
|
const replaceExtension = username => username
|
|
|
|
.replace(/\.(txt|jpg|jpeg|png|pdf|gif|doc|docx|csv|js|css|html)$/i, '_$1')
|
|
|
|
.replace(/\.$/, '')
|
|
|
|
;
|
2021-10-29 09:07:39 -07:00
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
export const saveAuthenticator = async (db, type, user, payload, validForMinutes = null) => {
|
2022-02-12 09:28:56 -08:00
|
|
|
if (await lookupBanArchive(db, type, payload)) {
|
|
|
|
throw 'banned';
|
|
|
|
}
|
2020-10-31 13:33:59 -07:00
|
|
|
const id = ulid();
|
|
|
|
await db.get(SQL`INSERT INTO authenticators (id, userId, type, payload, validUntil) VALUES (
|
|
|
|
${id},
|
|
|
|
${user ? user.id : null},
|
|
|
|
${type},
|
|
|
|
${JSON.stringify(payload)},
|
2020-11-02 12:45:45 -08:00
|
|
|
${validForMinutes ? (now() + validForMinutes * 60) : null}
|
2020-10-31 13:33:59 -07:00
|
|
|
)`);
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
export const findAuthenticatorById = async (db, id, type) => {
|
2020-10-31 13:33:59 -07:00
|
|
|
const authenticator = await db.get(SQL`SELECT * FROM authenticators
|
|
|
|
WHERE id = ${id}
|
|
|
|
AND type = ${type}
|
2020-11-02 12:45:45 -08:00
|
|
|
AND (validUntil IS NULL OR validUntil > ${now()})
|
2020-10-31 13:33:59 -07:00
|
|
|
`);
|
|
|
|
|
|
|
|
if (authenticator) {
|
|
|
|
authenticator.payload = JSON.parse(authenticator.payload);
|
|
|
|
}
|
|
|
|
|
|
|
|
return authenticator
|
|
|
|
}
|
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
export const findAuthenticatorsByUser = async (db, user, type) => {
|
|
|
|
const authenticators = await db.all(SQL`
|
|
|
|
SELECT * FROM authenticators
|
|
|
|
WHERE userId = ${user.id}
|
|
|
|
AND type = ${type}
|
|
|
|
AND (validUntil IS NULL OR validUntil > ${now()})
|
|
|
|
`);
|
|
|
|
|
|
|
|
return authenticators.map(a => {
|
|
|
|
a.payload = JSON.parse(a.payload);
|
|
|
|
|
|
|
|
return a;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-04-13 02:29:46 -07:00
|
|
|
const findLatestEmailAuthenticator = async (db, email, type) => {
|
|
|
|
const authenticator = await db.get(SQL`SELECT * FROM authenticators
|
|
|
|
WHERE payload LIKE ${'%"email":"' + email + '"%'}
|
|
|
|
AND type = ${type}
|
|
|
|
AND (validUntil IS NULL OR validUntil > ${now()})
|
|
|
|
ORDER BY id DESC
|
|
|
|
`);
|
|
|
|
|
|
|
|
if (authenticator) {
|
|
|
|
authenticator.payload = JSON.parse(authenticator.payload);
|
|
|
|
}
|
|
|
|
|
|
|
|
return authenticator
|
|
|
|
}
|
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
export const invalidateAuthenticator = async (db, id) => {
|
2020-10-31 13:33:59 -07:00
|
|
|
await db.get(SQL`UPDATE authenticators
|
2020-11-02 12:45:45 -08:00
|
|
|
SET validUntil = ${now()}
|
2020-10-31 13:33:59 -07:00
|
|
|
WHERE id = ${id}
|
|
|
|
`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const defaultUsername = async (db, email) => {
|
2021-07-14 07:05:34 -07:00
|
|
|
const base = normalise(
|
2021-10-29 09:07:39 -07:00
|
|
|
replaceExtension(
|
|
|
|
email.substring(0, email.includes('@') ? email.indexOf('@') : email.length)
|
|
|
|
.padEnd(4, '0')
|
|
|
|
.substring(0, 14)
|
|
|
|
.replace(new RegExp(`[^${USERNAME_CHARS}]`, 'g'), '_')
|
|
|
|
)
|
2021-07-14 07:05:34 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
const conflicts = (await db.all(SQL`SELECT usernameNorm FROM users WHERE usernameNorm LIKE ${normalise(base) + '%'}`))
|
|
|
|
.map(({usernameNorm}) => usernameNorm);
|
2020-10-31 13:33:59 -07:00
|
|
|
|
|
|
|
let c = 0;
|
|
|
|
while (true) {
|
|
|
|
let proposal = base + (c || '');
|
2021-07-14 07:05:34 -07:00
|
|
|
if (!conflicts.includes(proposal)) {
|
2020-10-31 13:33:59 -07:00
|
|
|
return proposal;
|
|
|
|
}
|
|
|
|
c++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-24 04:00:37 -08:00
|
|
|
const fetchOrCreateUser = async (db, user, avatarSource = 'gravatar') => {
|
2022-03-19 13:46:34 -07:00
|
|
|
let dbUser = user.email
|
|
|
|
? await db.get(SQL`SELECT * FROM users WHERE email = ${normalise(user.email)}`)
|
|
|
|
: await db.get(SQL`SELECT * FROM users WHERE usernameNorm = ${normalise(user.username)}`)
|
|
|
|
|
2020-10-31 13:33:59 -07:00
|
|
|
if (!dbUser) {
|
|
|
|
dbUser = {
|
|
|
|
id: ulid(),
|
2020-11-02 10:31:05 -08:00
|
|
|
username: await defaultUsername(db, user.name || user.email),
|
2020-10-31 13:33:59 -07:00
|
|
|
email: normalise(user.email),
|
2020-12-30 15:15:38 -08:00
|
|
|
roles: '',
|
2020-11-02 12:12:15 -08:00
|
|
|
avatarSource: avatarSource,
|
2020-10-31 13:33:59 -07:00
|
|
|
}
|
2021-07-14 06:28:53 -07:00
|
|
|
await db.get(SQL`INSERT INTO users(id, username, usernameNorm, email, roles, avatarSource)
|
|
|
|
VALUES (${dbUser.id}, ${dbUser.username}, ${normalise(dbUser.username)}, ${dbUser.email}, ${dbUser.roles}, ${dbUser.avatarSource})`)
|
2020-10-31 13:33:59 -07:00
|
|
|
}
|
|
|
|
|
2020-11-02 12:12:15 -08:00
|
|
|
dbUser.avatar = await avatar(db, dbUser);
|
|
|
|
|
2020-11-02 10:31:05 -08:00
|
|
|
return dbUser;
|
|
|
|
}
|
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
export const issueAuthentication = async (db, user, fetch = true, guardMfa = false, extend = undefined) => {
|
|
|
|
if (fetch) {
|
|
|
|
user = await fetchOrCreateUser(db, user);
|
|
|
|
}
|
2020-11-02 10:31:05 -08:00
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
if (user.mfa === undefined && user.id) {
|
|
|
|
user = await addMfaInfo(db, user, guardMfa);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!user.mfaRequired) {
|
|
|
|
user.authenticated = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
user.avatar = await avatar(db, user);
|
|
|
|
delete user.suspiciousChecked;
|
|
|
|
delete user.bannedBy;
|
2022-01-17 07:51:50 -08:00
|
|
|
delete user.banSnapshot;
|
2021-12-18 10:54:36 -08:00
|
|
|
|
|
|
|
if (extend) {
|
|
|
|
user = {
|
|
|
|
...user,
|
|
|
|
...extend,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return jwt.sign(user);
|
2020-10-31 13:33:59 -07:00
|
|
|
}
|
|
|
|
|
2021-04-12 06:43:16 -07:00
|
|
|
const validateEmail = async (email) => {
|
2021-09-09 10:58:45 -07:00
|
|
|
email = normalise(String(email));
|
2021-04-12 06:43:16 -07:00
|
|
|
if (email.endsWith('.oauth')) {
|
|
|
|
return;
|
|
|
|
}
|
2020-10-31 13:33:59 -07:00
|
|
|
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
2021-04-12 06:43:16 -07:00
|
|
|
if (!re.test(email)) {
|
|
|
|
return false;
|
|
|
|
}
|
2021-04-12 12:19:25 -07:00
|
|
|
const { Resolver } = require('dns').promises;
|
2021-04-12 06:43:16 -07:00
|
|
|
const dns = new Resolver();
|
|
|
|
try {
|
|
|
|
const addresses = await dns.resolveMx(email.split('@')[1]);
|
|
|
|
return addresses.length > 0;
|
|
|
|
} catch {
|
|
|
|
return false;
|
|
|
|
}
|
2020-10-31 13:33:59 -07:00
|
|
|
}
|
|
|
|
|
2021-04-13 02:29:46 -07:00
|
|
|
const deduplicateEmail = async (db, email, cbSuccess, cbFail) => {
|
|
|
|
const count = (await db.get(SQL`SELECT COUNT(*) AS c FROM emails WHERE email = ${email} AND sentAt >= ${now() - 5 * 60}`)).c;
|
|
|
|
if (count > 0) {
|
|
|
|
console.error('Duplicate email requests for ' + email);
|
|
|
|
if (cbFail) { await cbFail(); }
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
await cbSuccess();
|
|
|
|
await db.get(SQL`INSERT INTO emails (email, sentAt) VALUES (${email}, ${now()});`);
|
|
|
|
}
|
|
|
|
|
2020-11-03 00:27:30 -08:00
|
|
|
const reloadUser = async (req, res, next) => {
|
2021-06-17 16:43:17 -07:00
|
|
|
if (!req.url.startsWith('/user/') && req.method === 'GET') {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-03 00:27:30 -08:00
|
|
|
if (!req.user) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
let dbUser = await req.db.get(SQL`SELECT * FROM users WHERE id = ${req.user.id}`);
|
2020-11-03 00:27:30 -08:00
|
|
|
|
|
|
|
if (!dbUser) {
|
|
|
|
res.clearCookie('token');
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
dbUser = await addMfaInfo(req.db, dbUser);
|
|
|
|
|
2021-11-23 05:44:24 -08:00
|
|
|
await req.db.get(SQL`UPDATE users SET lastActive = ${+new Date} WHERE id = ${req.user.id}`);
|
|
|
|
|
2020-11-03 00:27:30 -08:00
|
|
|
if (req.user.username !== dbUser.username
|
|
|
|
|| req.user.email !== dbUser.email
|
|
|
|
|| req.user.roles !== dbUser.roles
|
|
|
|
|| req.user.avatarSource !== dbUser.avatarSource
|
2021-06-16 07:08:38 -07:00
|
|
|
|| req.user.bannedReason !== dbUser.bannedReason
|
2021-12-18 10:54:36 -08:00
|
|
|
|| req.user.mfa !== dbUser.mfa
|
2020-11-03 00:27:30 -08:00
|
|
|
) {
|
2021-12-18 10:54:36 -08:00
|
|
|
const token = await issueAuthentication(req.db, dbUser, false);
|
2021-02-04 04:11:47 -08:00
|
|
|
res.cookie('token', token, cookieSettings);
|
2021-12-18 10:54:36 -08:00
|
|
|
req.rawUser = jwt.validate(token);
|
|
|
|
req.user = req.rawUser;
|
2020-11-03 00:27:30 -08:00
|
|
|
}
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
|
2021-10-15 07:15:13 -07:00
|
|
|
const resetCards = async (db, id) => {
|
|
|
|
await db.get(SQL`UPDATE profiles SET card = null, cardDark = null WHERE userId = ${id}`);
|
|
|
|
}
|
|
|
|
|
2020-10-31 13:33:59 -07:00
|
|
|
const router = Router();
|
|
|
|
|
2021-06-09 09:13:18 -07:00
|
|
|
router.use(handleErrorAsync(reloadUser));
|
2020-11-03 00:27:30 -08:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.post('/user/init', handleErrorAsync(async (req, res) => {
|
2021-08-07 02:14:53 -07:00
|
|
|
if (req.body.usernameOrEmail && isSpam(req.body.usernameOrEmail || '')) {
|
2021-08-06 15:00:42 -07:00
|
|
|
req.socket.end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-08-07 03:03:49 -07:00
|
|
|
if (!await validateCaptcha(req.body.captchaToken)) {
|
|
|
|
return res.json({error: 'captcha.invalid'});
|
|
|
|
}
|
|
|
|
|
2020-10-31 13:33:59 -07:00
|
|
|
let user = undefined;
|
|
|
|
let usernameOrEmail = req.body.usernameOrEmail;
|
|
|
|
|
|
|
|
const isEmail = usernameOrEmail.indexOf('@') > -1;
|
|
|
|
let isTest = false;
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV === 'development' && usernameOrEmail.endsWith('+')) {
|
|
|
|
isTest = true;
|
|
|
|
usernameOrEmail = usernameOrEmail.substring(0, usernameOrEmail.length - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isEmail) {
|
|
|
|
user = await req.db.get(SQL`SELECT * FROM users WHERE email = ${normalise(usernameOrEmail)}`);
|
|
|
|
} else {
|
2021-07-14 06:28:53 -07:00
|
|
|
user = await req.db.get(SQL`SELECT * FROM users WHERE usernameNorm = ${normalise(usernameOrEmail)}`);
|
2020-10-31 13:33:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!user && !isEmail) {
|
|
|
|
return res.json({error: 'user.login.userNotFound'})
|
|
|
|
}
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
username: isEmail ? (user ? user.username : null) : usernameOrEmail,
|
|
|
|
email: isEmail ? normalise(usernameOrEmail) : user.email,
|
|
|
|
code: isTest ? '999999' : makeId(6, '0123456789'),
|
|
|
|
}
|
|
|
|
|
2021-04-12 06:43:16 -07:00
|
|
|
if (!await validateEmail(payload.email)) {
|
|
|
|
return res.json({ error: 'user.account.changeEmail.invalid' })
|
|
|
|
}
|
|
|
|
|
2021-04-13 02:29:46 -07:00
|
|
|
let codeKey;
|
|
|
|
if (isTest) {
|
|
|
|
codeKey = await saveAuthenticator(req.db, 'email', user, payload, 15);
|
|
|
|
} else {
|
|
|
|
await deduplicateEmail(
|
|
|
|
req.db,
|
2020-10-31 13:33:59 -07:00
|
|
|
payload.email,
|
2021-04-13 02:29:46 -07:00
|
|
|
async () => {
|
|
|
|
codeKey = await saveAuthenticator(req.db, 'email', user, payload, 15);
|
|
|
|
|
2021-12-03 13:39:08 -08:00
|
|
|
mailer(payload.email, 'confirmCode', { code: payload.code });
|
2021-04-13 02:29:46 -07:00
|
|
|
},
|
|
|
|
async () => {
|
|
|
|
const auth = await findLatestEmailAuthenticator(req.db, payload.email, 'email');
|
|
|
|
codeKey = auth ? auth.id : null;
|
|
|
|
},
|
|
|
|
);
|
2020-10-31 13:33:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return res.json({
|
2022-03-19 13:46:34 -07:00
|
|
|
token: jwt.sign(
|
|
|
|
{
|
|
|
|
...payload,
|
|
|
|
email: isEmail ? payload.email : null,
|
|
|
|
emailObfuscated: obfuscateEmail(payload.email),
|
|
|
|
code: null,
|
|
|
|
codeKey,
|
|
|
|
},
|
|
|
|
'15m',
|
|
|
|
),
|
2020-10-31 13:33:59 -07:00
|
|
|
});
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-10-31 13:33:59 -07:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.post('/user/validate', handleErrorAsync(async (req, res) => {
|
2020-10-31 13:33:59 -07:00
|
|
|
if (!req.rawUser || !req.rawUser.codeKey) {
|
|
|
|
return res.json({error: 'user.tokenExpired'});
|
|
|
|
}
|
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
const authenticator = await findAuthenticatorById(req.db, req.rawUser.codeKey, 'email');
|
2020-10-31 13:33:59 -07:00
|
|
|
if (!authenticator) {
|
|
|
|
return res.json({error: 'user.tokenExpired'});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (authenticator.payload.code !== normalise(req.body.code)) {
|
|
|
|
return res.json({error: 'user.code.invalid'});
|
|
|
|
}
|
|
|
|
|
|
|
|
await invalidateAuthenticator(req.db, authenticator);
|
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
return res.json({token: await issueAuthentication(req.db, req.rawUser, true, true)});
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-10-31 13:33:59 -07:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.post('/user/change-username', handleErrorAsync(async (req, res) => {
|
2020-10-31 13:33:59 -07:00
|
|
|
if (!req.user) {
|
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (req.body.username.length < 4 || req.body.username.length > 16 || !req.body.username.match(new RegExp(`^[${USERNAME_CHARS}]+$`))) {
|
2021-04-13 09:47:54 -07:00
|
|
|
return res.json({ error: 'user.account.changeUsername.invalid' });
|
2020-10-31 13:33:59 -07:00
|
|
|
}
|
|
|
|
|
2021-10-29 09:07:39 -07:00
|
|
|
req.body.username = replaceExtension(req.body.username);
|
|
|
|
|
2021-07-14 06:28:53 -07:00
|
|
|
const dbUser = await req.db.get(SQL`SELECT * FROM users WHERE usernameNorm = ${normalise(req.body.username)}`);
|
2021-09-07 13:21:30 -07:00
|
|
|
if (dbUser && dbUser.id !== req.user.id) {
|
2020-10-31 13:33:59 -07:00
|
|
|
return res.json({ error: 'user.account.changeUsername.taken' })
|
|
|
|
}
|
|
|
|
|
2021-07-14 06:28:53 -07:00
|
|
|
await req.db.get(SQL`UPDATE users SET username = ${req.body.username}, usernameNorm = ${normalise(req.body.username)} WHERE id = ${req.user.id}`);
|
2020-10-31 13:33:59 -07:00
|
|
|
|
2021-10-15 07:15:13 -07:00
|
|
|
await resetCards(req.db, req.user.id);
|
|
|
|
|
2020-11-02 10:31:05 -08:00
|
|
|
return res.json({token: await issueAuthentication(req.db, req.user)});
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-10-31 13:33:59 -07:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.post('/user/change-email', handleErrorAsync(async (req, res) => {
|
2021-08-07 02:14:53 -07:00
|
|
|
if (!req.user || req.user.bannedReason || isSpam(req.body.email || '')) {
|
2020-10-31 13:33:59 -07:00
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
|
|
|
}
|
|
|
|
|
2021-09-09 11:09:06 -07:00
|
|
|
if (!await validateEmail(normalise(req.body.email))) {
|
2020-10-31 13:33:59 -07:00
|
|
|
return res.json({ error: 'user.account.changeEmail.invalid' })
|
|
|
|
}
|
|
|
|
|
|
|
|
const dbUser = await req.db.get(SQL`SELECT * FROM users WHERE lower(trim(email)) = ${normalise(req.body.email)}`);
|
|
|
|
if (dbUser) {
|
|
|
|
return res.json({ error: 'user.account.changeEmail.taken' })
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!req.body.authId) {
|
2021-08-09 13:58:42 -07:00
|
|
|
if (!await validateCaptcha(req.body.captchaToken)) {
|
|
|
|
return res.json({error: 'captcha.invalid'});
|
|
|
|
}
|
|
|
|
|
2020-10-31 13:33:59 -07:00
|
|
|
const payload = {
|
|
|
|
from: req.user.email,
|
|
|
|
to: normalise(req.body.email),
|
|
|
|
code: makeId(6, '0123456789'),
|
|
|
|
};
|
|
|
|
|
|
|
|
const authId = await saveAuthenticator(req.db, 'changeEmail', req.user, payload, 15);
|
|
|
|
|
2021-12-03 13:39:08 -08:00
|
|
|
mailer(payload.to, 'confirmCode', { code: payload.code });
|
2020-10-31 13:33:59 -07:00
|
|
|
|
|
|
|
return res.json({ authId });
|
|
|
|
}
|
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
const authenticator = await findAuthenticatorById(req.db, req.body.authId, 'changeEmail');
|
2020-10-31 13:33:59 -07:00
|
|
|
if (!authenticator) {
|
|
|
|
return res.json({error: 'user.tokenExpired'});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (authenticator.payload.code !== normalise(req.body.code)) {
|
|
|
|
return res.json({error: 'user.code.invalid'});
|
|
|
|
}
|
|
|
|
|
|
|
|
await invalidateAuthenticator(req.db, authenticator);
|
|
|
|
|
2020-11-03 00:27:30 -08:00
|
|
|
await req.db.get(SQL`UPDATE users SET email = ${authenticator.payload.to} WHERE id = ${req.user.id}`);
|
2020-10-31 13:33:59 -07:00
|
|
|
req.user.email = authenticator.payload.to;
|
|
|
|
|
2020-11-02 10:31:05 -08:00
|
|
|
return res.json({token: await issueAuthentication(req.db, req.user)});
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-10-31 13:33:59 -07:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.post('/user/delete', handleErrorAsync(async (req, res) => {
|
2020-10-31 13:33:59 -07:00
|
|
|
if (!req.user) {
|
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
|
|
|
}
|
|
|
|
|
2022-01-09 15:13:41 -08:00
|
|
|
await req.db.get('PRAGMA foreign_keys = ON')
|
2020-11-03 00:27:30 -08:00
|
|
|
await req.db.get(SQL`DELETE FROM users WHERE id = ${req.user.id}`)
|
2020-10-31 13:33:59 -07:00
|
|
|
|
|
|
|
return res.json(true);
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-10-31 13:33:59 -07:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.post('/user/:id/set-roles', handleErrorAsync(async (req, res) => {
|
2020-12-30 15:03:30 -08:00
|
|
|
if (!req.isGranted('*')) {
|
2020-11-03 01:03:07 -08:00
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
|
|
|
}
|
|
|
|
|
|
|
|
await req.db.get(SQL`UPDATE users SET roles = ${req.body.roles} WHERE id = ${req.params.id}`);
|
|
|
|
|
|
|
|
return res.json('ok');
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-11-03 01:03:07 -08:00
|
|
|
|
2021-12-03 08:39:14 -08:00
|
|
|
// happens on home
|
|
|
|
router.get('/user/social-redirect/:provider/:locale', handleErrorAsync(async (req, res) => {
|
2021-12-14 15:45:28 -08:00
|
|
|
assert(req.locales.hasOwnProperty(req.params.locale));
|
2021-12-03 08:39:14 -08:00
|
|
|
req.session.socialRedirect = req.params.locale;
|
2021-12-05 11:56:39 -08:00
|
|
|
return res.redirect(`/api/connect/${req.params.provider}?${new URLSearchParams({
|
|
|
|
instance: req.query.instance || undefined,
|
|
|
|
})}`);
|
2021-12-03 08:39:14 -08:00
|
|
|
}));
|
|
|
|
|
|
|
|
// happens on home
|
2021-06-09 05:47:08 -07:00
|
|
|
router.get('/user/social/:provider', handleErrorAsync(async (req, res) => {
|
2020-11-02 23:36:15 -08:00
|
|
|
if (!req.session.grant || !req.session.grant.response || !req.session.grant.response.access_token || !socialLoginHandlers[req.params.provider]) {
|
|
|
|
return res.status(400).redirect('/' + config.user.route);
|
|
|
|
}
|
|
|
|
|
2021-06-07 10:03:06 -07:00
|
|
|
const payload = socialLoginHandlers[req.params.provider](req.session.grant.response);
|
|
|
|
|
|
|
|
if (payload.id === undefined) {
|
|
|
|
return res.status(400).redirect('/' + config.user.route);
|
|
|
|
}
|
2020-11-02 10:31:05 -08:00
|
|
|
|
|
|
|
const auth = await req.db.get(SQL`
|
|
|
|
SELECT * FROM authenticators
|
|
|
|
WHERE type = ${req.params.provider}
|
|
|
|
AND payload LIKE ${'{"id":"' + payload.id + '"%'}
|
2020-11-02 12:45:45 -08:00
|
|
|
AND (validUntil IS NULL OR validUntil > ${now()})
|
2020-11-02 10:31:05 -08:00
|
|
|
`)
|
|
|
|
|
|
|
|
const user = auth ? await req.db.get(SQL`
|
|
|
|
SELECT * FROM users
|
|
|
|
WHERE id = ${auth.userId}
|
|
|
|
`) : req.user;
|
|
|
|
|
|
|
|
const dbUser = await fetchOrCreateUser(req.db, user || {
|
|
|
|
email: payload.email || `${payload.id}@${req.params.provider}.oauth`,
|
|
|
|
name: payload.name,
|
2020-11-02 12:12:15 -08:00
|
|
|
}, req.params.provider);
|
2020-11-02 10:31:05 -08:00
|
|
|
|
2021-12-18 10:54:36 -08:00
|
|
|
const token = await issueAuthentication(req.db, dbUser, false, true);
|
2020-11-02 10:31:05 -08:00
|
|
|
|
|
|
|
if (auth) {
|
|
|
|
await invalidateAuthenticator(req.db, auth.id);
|
|
|
|
}
|
|
|
|
await saveAuthenticator(req.db, req.params.provider, dbUser, payload);
|
|
|
|
|
2021-12-03 08:39:14 -08:00
|
|
|
const buildRedirectUrl = () => {
|
|
|
|
if (!req.session.socialRedirect) {
|
|
|
|
return '/' + config.user.route;
|
|
|
|
}
|
2021-12-05 14:05:51 -08:00
|
|
|
const host = process.env.NODE_ENV === 'development' ? '' : buildLocaleList(config.locale, true)[req.session.socialRedirect].url;
|
2021-12-03 08:39:14 -08:00
|
|
|
delete req.session.socialRedirect;
|
|
|
|
|
|
|
|
return `${host}/api/user/social-redirect-callback/${encodeURIComponent(token)}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.cookie('token', token, cookieSettings).redirect(buildRedirectUrl());
|
|
|
|
}));
|
|
|
|
|
|
|
|
// happens on locale
|
|
|
|
router.get('/user/social-redirect-callback/:token', handleErrorAsync(async (req, res) => {
|
|
|
|
res.cookie('token', req.params.token, cookieSettings).redirect('/' + config.user.route);
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-11-02 10:31:05 -08:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.get('/user/social-connections', handleErrorAsync(async (req, res) => {
|
2020-11-02 10:31:05 -08:00
|
|
|
if (!req.user) {
|
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
|
|
|
}
|
|
|
|
|
|
|
|
const authenticators = await req.db.all(SQL`
|
|
|
|
SELECT type, payload FROM authenticators
|
2020-11-02 12:45:45 -08:00
|
|
|
WHERE type IN (`.append(Object.keys(socialLoginConfig).map(k => `'${k}'`).join(',')).append(SQL`)
|
2020-11-02 10:31:05 -08:00
|
|
|
AND userId = ${req.user.id}
|
2020-11-02 12:45:45 -08:00
|
|
|
AND (validUntil IS NULL OR validUntil > ${now()})
|
2020-11-02 10:31:05 -08:00
|
|
|
`));
|
|
|
|
|
|
|
|
return res.json(buildDict(function* () {
|
|
|
|
for (let auth of authenticators) {
|
|
|
|
yield [auth.type, JSON.parse(auth.payload)];
|
|
|
|
}
|
|
|
|
}));
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-11-02 10:31:05 -08:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.post('/user/social-connection/:provider/disconnect', handleErrorAsync(async (req, res) => {
|
2020-11-02 10:31:05 -08:00
|
|
|
if (!req.user) {
|
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
|
|
|
}
|
|
|
|
|
|
|
|
const auth = await req.db.get(SQL`
|
|
|
|
SELECT id FROM authenticators
|
|
|
|
WHERE type = ${req.params.provider}
|
|
|
|
AND userId = ${req.user.id}
|
2020-11-02 12:45:45 -08:00
|
|
|
AND (validUntil IS NULL OR validUntil > ${now()})
|
2020-11-02 10:31:05 -08:00
|
|
|
`)
|
|
|
|
|
|
|
|
await invalidateAuthenticator(req.db, auth.id)
|
|
|
|
|
|
|
|
return res.json('ok');
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-11-02 10:31:05 -08:00
|
|
|
|
2021-06-09 05:47:08 -07:00
|
|
|
router.post('/user/set-avatar', handleErrorAsync(async (req, res) => {
|
2020-11-02 12:12:15 -08:00
|
|
|
if (!req.user) {
|
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
|
|
|
}
|
|
|
|
|
|
|
|
await req.db.get(SQL`
|
|
|
|
UPDATE users
|
|
|
|
SET avatarSource = ${req.body.source || null}
|
|
|
|
WHERE id = ${req.user.id}
|
|
|
|
`)
|
|
|
|
|
2021-10-15 07:15:13 -07:00
|
|
|
await resetCards(req.db, req.user.id);
|
|
|
|
|
2020-11-02 12:12:15 -08:00
|
|
|
return res.json({token: await issueAuthentication(req.db, req.user)});
|
2021-06-09 05:47:08 -07:00
|
|
|
}));
|
2020-11-02 12:12:15 -08:00
|
|
|
|
2021-08-22 14:53:22 -07:00
|
|
|
router.get('/user/init-universal/:token', handleErrorAsync(async (req, res) => {
|
2021-08-23 00:43:23 -07:00
|
|
|
res.header('Access-Control-Allow-Origin', '*');
|
2021-08-22 15:05:49 -07:00
|
|
|
if (req.user) {
|
2021-08-22 14:53:22 -07:00
|
|
|
return res.json('Already logged in');
|
|
|
|
}
|
|
|
|
|
|
|
|
res.cookie('token', req.params.token, cookieSettings);
|
|
|
|
return res.json('Token saved');
|
|
|
|
}));
|
|
|
|
|
2021-08-23 00:43:23 -07:00
|
|
|
router.get('/user/logout-universal', handleErrorAsync(async (req, res) => {
|
|
|
|
res.header('Access-Control-Allow-Origin', '*');
|
|
|
|
res.clearCookie('token');
|
|
|
|
return res.json('Token removed');
|
|
|
|
}));
|
|
|
|
|
2022-01-15 12:50:33 -08:00
|
|
|
const canImpersonate = (req) => {
|
|
|
|
return req.isGranted('*') || (
|
|
|
|
req.isGranted('users') && ['example@pronouns.page'].includes(req.params.email)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-11-28 03:19:37 -08:00
|
|
|
router.get('/admin/impersonate/:email', handleErrorAsync(async (req, res) => {
|
2022-01-15 12:50:33 -08:00
|
|
|
if (!canImpersonate(req)) {
|
2021-11-28 03:19:37 -08:00
|
|
|
return res.status(401).json({error: 'Unauthorised'});
|
|
|
|
}
|
|
|
|
|
2021-12-02 10:11:04 -08:00
|
|
|
return res.json({token: await issueAuthentication(req.db, {email: req.params.email})});
|
2021-11-28 03:19:37 -08:00
|
|
|
}));
|
|
|
|
|
2020-10-31 13:33:59 -07:00
|
|
|
export default router;
|