2020-10-13 12:49:08 -07:00
|
|
|
import jwt from './jwt';
|
2020-10-16 14:32:51 -07:00
|
|
|
import {makeId, renderJson} from '../src/helpers';
|
2020-10-14 12:49:18 -07:00
|
|
|
const dbConnection = require('./db');
|
|
|
|
const SQL = require('sql-template-strings');
|
|
|
|
import { ulid } from 'ulid';
|
|
|
|
import translations from "./translations";
|
|
|
|
const mailer = require('./mailer');
|
2020-10-15 11:55:24 -07:00
|
|
|
import authenticate from './authenticate';
|
2020-10-14 12:49:18 -07:00
|
|
|
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
|
2020-10-15 11:29:56 -07:00
|
|
|
const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';
|
|
|
|
|
2020-10-15 13:35:13 -07:00
|
|
|
const normalise = s => s.trim().toLowerCase();
|
|
|
|
|
2020-10-14 12:49:18 -07:00
|
|
|
const saveAuthenticator = async (db, type, user, payload, validForMinutes = null) => {
|
|
|
|
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)},
|
|
|
|
${validForMinutes ? (now + validForMinutes * 60) : null}
|
|
|
|
)`);
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
const findAuthenticator = async (db, id, type) => {
|
|
|
|
const authenticator = await db.get(SQL`SELECT * FROM authenticators
|
|
|
|
WHERE id = ${id}
|
|
|
|
AND type = ${type}
|
|
|
|
AND (validUntil IS NULL OR validUntil > ${now})
|
|
|
|
`);
|
|
|
|
|
|
|
|
if (authenticator) {
|
|
|
|
authenticator.payload = JSON.parse(authenticator.payload);
|
|
|
|
}
|
|
|
|
|
|
|
|
return authenticator
|
|
|
|
}
|
|
|
|
|
|
|
|
const invalidateAuthenticator = async (db, id) => {
|
|
|
|
await db.get(SQL`UPDATE authenticators
|
|
|
|
SET validUntil = ${now}
|
|
|
|
WHERE id = ${id}
|
|
|
|
`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const init = async (db, usernameOrEmail) => {
|
|
|
|
let user = undefined;
|
|
|
|
|
|
|
|
const isEmail = usernameOrEmail.indexOf('@') > -1;
|
2020-10-15 09:50:05 -07:00
|
|
|
let isTest = false;
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV === 'development' && usernameOrEmail.endsWith('+')) {
|
|
|
|
isTest = true;
|
|
|
|
usernameOrEmail = usernameOrEmail.substring(0, usernameOrEmail.length - 1);
|
|
|
|
}
|
2020-10-14 12:49:18 -07:00
|
|
|
|
|
|
|
if (isEmail) {
|
2020-10-15 13:35:13 -07:00
|
|
|
user = await db.get(SQL`SELECT * FROM users WHERE email = ${normalise(usernameOrEmail)}`);
|
2020-10-14 12:49:18 -07:00
|
|
|
} else {
|
2020-10-15 13:35:13 -07:00
|
|
|
user = await db.get(SQL`SELECT * FROM users WHERE lower(trim(username)) = ${normalise(usernameOrEmail)}`);
|
2020-10-14 12:49:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!user && !isEmail) {
|
|
|
|
return {error: 'user.login.userNotFound'}
|
|
|
|
}
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
username: isEmail ? (user ? user.username : null) : usernameOrEmail,
|
2020-10-15 13:35:13 -07:00
|
|
|
email: isEmail ? normalise(usernameOrEmail) : user.email,
|
2020-10-15 09:50:05 -07:00
|
|
|
code: isTest ? '999999' : makeId(6, '0123456789'),
|
2020-10-14 12:49:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const codeKey = await saveAuthenticator(db, 'email', user, payload, 15);
|
|
|
|
|
2020-10-15 09:50:05 -07:00
|
|
|
if (!isTest) {
|
|
|
|
mailer(
|
|
|
|
payload.email,
|
|
|
|
`[${translations.title}] ${translations.user.login.email.subject.replace('%code%', payload.code)}`,
|
|
|
|
translations.user.login.email.content.replace('%code%', payload.code),
|
|
|
|
)
|
|
|
|
}
|
2020-10-14 12:49:18 -07:00
|
|
|
|
|
|
|
return {
|
|
|
|
token: jwt.sign({...payload, code: null, codeKey}, '15m'),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const validate = async (db, user, code) => {
|
|
|
|
if (!user || !user.codeKey) {
|
|
|
|
return {error: 'user.tokenExpired'};
|
|
|
|
}
|
|
|
|
|
|
|
|
const authenticator = await findAuthenticator(db, user.codeKey, 'email');
|
|
|
|
if (!authenticator) {
|
|
|
|
return {error: 'user.tokenExpired'};
|
|
|
|
}
|
|
|
|
|
2020-10-15 13:35:13 -07:00
|
|
|
if (authenticator.payload.code !== normalise(code)) {
|
2020-10-14 12:49:18 -07:00
|
|
|
return {error: 'user.code.invalid'};
|
|
|
|
}
|
|
|
|
|
2020-10-15 11:29:56 -07:00
|
|
|
await invalidateAuthenticator(db, authenticator);
|
|
|
|
|
2020-10-15 11:55:24 -07:00
|
|
|
return await issueAuthentication(db, user);
|
2020-10-14 12:49:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const defaultUsername = async (db, email) => {
|
|
|
|
const base = email.substring(0, email.indexOf('@'))
|
|
|
|
.padEnd(4, '0')
|
2020-10-15 09:50:05 -07:00
|
|
|
.substring(0, 12)
|
2020-10-15 11:29:56 -07:00
|
|
|
.replace(new RegExp(`[^${USERNAME_CHARS}]`, 'g'), '_');
|
2020-10-14 12:49:18 -07:00
|
|
|
|
|
|
|
let c = 0;
|
|
|
|
while (true) {
|
|
|
|
let proposal = base + (c || '');
|
2020-10-15 13:35:13 -07:00
|
|
|
let dbUser = await db.get(SQL`SELECT id FROM users WHERE lower(trim(username)) = ${normalise(proposal)}`);
|
2020-10-14 12:49:18 -07:00
|
|
|
if (!dbUser) {
|
|
|
|
return proposal;
|
|
|
|
}
|
|
|
|
c++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-15 11:55:24 -07:00
|
|
|
const issueAuthentication = async (db, user) => {
|
2020-10-15 13:35:13 -07:00
|
|
|
let dbUser = await db.get(SQL`SELECT * FROM users WHERE email = ${normalise(user.email)}`);
|
2020-10-14 12:49:18 -07:00
|
|
|
if (!dbUser) {
|
|
|
|
dbUser = {
|
|
|
|
id: ulid(),
|
|
|
|
username: await defaultUsername(db, user.email),
|
2020-10-15 13:35:13 -07:00
|
|
|
email: normalise(user.email),
|
2020-10-14 12:49:18 -07:00
|
|
|
roles: 'user',
|
|
|
|
avatarSource: null,
|
|
|
|
}
|
2020-10-15 12:23:07 -07:00
|
|
|
await db.get(SQL`INSERT INTO users(id, username, email, roles, avatarSource)
|
|
|
|
VALUES (${dbUser.id}, ${dbUser.username}, ${dbUser.email}, ${dbUser.roles}, ${dbUser.avatarSource})`)
|
2020-10-14 12:49:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
token: jwt.sign({
|
|
|
|
...dbUser,
|
|
|
|
authenticated: true,
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
2020-10-13 12:49:08 -07:00
|
|
|
|
2020-10-15 11:29:56 -07:00
|
|
|
const changeUsername = async (db, user, username) => {
|
|
|
|
if (username.length < 4 || username.length > 16 || !username.match(new RegExp(`^[${USERNAME_CHARS}]+$`))) {
|
|
|
|
return { error: 'user.account.changeUsername.invalid' }
|
|
|
|
}
|
|
|
|
|
2020-10-15 13:35:13 -07:00
|
|
|
const dbUser = await db.get(SQL`SELECT * FROM users WHERE lower(trim(username)) = ${normalise(username)}`);
|
2020-10-15 11:29:56 -07:00
|
|
|
if (dbUser) {
|
|
|
|
return { error: 'user.account.changeUsername.taken' }
|
|
|
|
}
|
|
|
|
|
2020-10-15 13:35:13 -07:00
|
|
|
await db.get(SQL`UPDATE users SET username = ${username} WHERE email = ${normalise(user.email)}`);
|
2020-10-15 11:29:56 -07:00
|
|
|
|
2020-10-15 11:55:24 -07:00
|
|
|
return await issueAuthentication(db, user);
|
2020-10-15 11:29:56 -07:00
|
|
|
}
|
|
|
|
|
2020-10-27 10:15:18 -07:00
|
|
|
const validateEmail = (email) => {
|
|
|
|
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,}))$/;
|
|
|
|
return re.test(String(email).toLowerCase());
|
|
|
|
}
|
|
|
|
|
|
|
|
const changeEmail = async (db, user, email, authId, code) => {
|
|
|
|
if (!validateEmail(email)) {
|
|
|
|
return { error: 'user.account.changeEmail.invalid' }
|
|
|
|
}
|
|
|
|
|
|
|
|
const dbUser = await db.get(SQL`SELECT * FROM users WHERE lower(trim(email)) = ${normalise(email)}`);
|
|
|
|
if (dbUser) {
|
|
|
|
return { error: 'user.account.changeEmail.taken' }
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!authId) {
|
|
|
|
const payload = {
|
|
|
|
from: user.email,
|
|
|
|
to: normalise(email),
|
|
|
|
code: makeId(6, '0123456789'),
|
|
|
|
};
|
|
|
|
|
|
|
|
const authId = await saveAuthenticator(db, 'changeEmail', user, payload, 15);
|
|
|
|
|
|
|
|
mailer(
|
|
|
|
payload.to,
|
|
|
|
`[${translations.title}] ${translations.user.login.email.subject.replace('%code%', payload.code)}`,
|
|
|
|
translations.user.login.email.content.replace('%code%', payload.code),
|
|
|
|
)
|
|
|
|
|
|
|
|
return { authId };
|
|
|
|
}
|
|
|
|
|
|
|
|
const authenticator = await findAuthenticator(db, authId, 'changeEmail');
|
|
|
|
if (!authenticator) {
|
|
|
|
return {error: 'user.tokenExpired'};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (authenticator.payload.code !== normalise(code)) {
|
|
|
|
return {error: 'user.code.invalid'};
|
|
|
|
}
|
|
|
|
|
|
|
|
await invalidateAuthenticator(db, authenticator);
|
|
|
|
|
|
|
|
await db.get(SQL`UPDATE users SET email = ${authenticator.payload.to} WHERE email = ${normalise(user.email)}`);
|
|
|
|
user.email = authenticator.payload.to;
|
|
|
|
|
|
|
|
return await issueAuthentication(db, user);
|
|
|
|
}
|
|
|
|
|
2020-10-27 08:33:45 -07:00
|
|
|
const removeAccount = async (db, user) => {
|
|
|
|
const userId = (await db.get(SQL`SELECT id FROM users WHERE username = ${user.username}`)).id;
|
|
|
|
if (!userId) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
await db.get(SQL`DELETE FROM profiles WHERE userId = ${userId}`)
|
|
|
|
await db.get(SQL`DELETE FROM authenticators WHERE userId = ${userId}`)
|
|
|
|
await db.get(SQL`DELETE FROM users WHERE id = ${userId}`)
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-10-13 12:49:08 -07:00
|
|
|
export default async function (req, res, next) {
|
|
|
|
const db = await dbConnection();
|
2020-10-15 11:55:24 -07:00
|
|
|
const user = authenticate(req);
|
2020-10-13 12:49:08 -07:00
|
|
|
|
2020-10-14 12:49:18 -07:00
|
|
|
if (req.method === 'POST' && req.url === '/init' && req.body.usernameOrEmail) {
|
2020-10-16 14:46:49 -07:00
|
|
|
return renderJson(res, await init(db, req.body.usernameOrEmail));
|
2020-10-16 14:32:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (req.method === 'POST' && req.url === '/validate' && req.body.code) {
|
2020-10-16 14:46:49 -07:00
|
|
|
return renderJson(res, await validate(db, user, req.body.code));
|
2020-10-16 14:32:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (req.method === 'POST' && req.url === '/change-username' && user && user.authenticated && req.body.username) {
|
2020-10-16 14:46:49 -07:00
|
|
|
return renderJson(res, await changeUsername(db, user, req.body.username));
|
2020-10-13 12:49:08 -07:00
|
|
|
}
|
|
|
|
|
2020-10-27 10:15:18 -07:00
|
|
|
if (req.method === 'POST' && req.url === '/change-email' && user && user.authenticated && req.body.email) {
|
|
|
|
return renderJson(res, await changeEmail(db, user, req.body.email, req.body.authId, req.body.code));
|
|
|
|
}
|
|
|
|
|
2020-10-27 08:33:45 -07:00
|
|
|
if (req.method === 'POST' && req.url === '/delete' && user && user.authenticated) {
|
|
|
|
return renderJson(res, await removeAccount(db, user));
|
|
|
|
}
|
|
|
|
|
2020-10-16 14:32:51 -07:00
|
|
|
return renderJson(res, {error: 'Not found'}, 404);
|
2020-10-13 12:49:08 -07:00
|
|
|
}
|