#87 move backend to express
This commit is contained in:
parent
b5521eab47
commit
c8abcaf9f7
|
@ -36,11 +36,11 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
async removeProfile() {
|
||||
async removeProfile(locale) {
|
||||
await this.$confirm(this.$t('profile.deleteConfirm'), 'danger');
|
||||
|
||||
this.deleting = true;
|
||||
const response = await this.$axios.$post(`/profile/delete/${this.config.locale}`, {}, { headers: this.$auth() });
|
||||
const response = await this.$axios.$post(`/profile/delete/${locale}`, {}, { headers: this.$auth() });
|
||||
this.deleting = false;
|
||||
this.$emit('update', response);
|
||||
},
|
||||
|
|
|
@ -86,6 +86,7 @@ export default {
|
|||
},
|
||||
env: {
|
||||
BASE_URL: process.env.BASE_URL,
|
||||
TITLE: title,
|
||||
PUBLIC_KEY: fs.readFileSync(__dirname + '/keys/public.pem').toString(),
|
||||
LOCALE: config.locale,
|
||||
FLAGS: buildDict(function *() {
|
||||
|
@ -102,15 +103,7 @@ export default {
|
|||
}
|
||||
}),
|
||||
},
|
||||
serverMiddleware: {
|
||||
'/': bodyParser.json(),
|
||||
'/banner': '~/server/banner.js',
|
||||
'/api/nouns': '~/server/nouns.js',
|
||||
'/api/user': '~/server/user.js',
|
||||
'/api/profile': '~/server/profile.js',
|
||||
'/api/admin': '~/server/admin.js',
|
||||
'/api/sources': '~/server/sources.js',
|
||||
},
|
||||
serverMiddleware: ['~/server/index.js'],
|
||||
axios: {
|
||||
baseURL: process.env.BASE_URL + '/api',
|
||||
},
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"canvas": "^2.6.1",
|
||||
"cookie-universal-nuxt": "^2.1.4",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"js-base64": "^3.5.2",
|
||||
"js-md5": "^0.7.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
head() {
|
||||
return head({
|
||||
title: `${this.$t('template.intro')}: ${this.$t('template.any.short')}`,
|
||||
banner: `banner/${this.$t('template.any.short')}.png`,
|
||||
banner: `api/banner/${this.$t('template.any.short')}.png`,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</h2>
|
||||
<div>
|
||||
<nuxt-link v-if="$user() && $user().username === username" :to="`/${config.profile.editorRoute}`"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
class="btn btn-outline-primary btn-sm mb-2"
|
||||
>
|
||||
<Icon v="edit"/>
|
||||
<T>profile.edit</T>
|
||||
|
@ -158,7 +158,7 @@
|
|||
head() {
|
||||
return head({
|
||||
title: `@${this.username}`,
|
||||
banner: `banner/@${this.username}.png`,
|
||||
banner: `api/banner/@${this.username}.png`,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@
|
|||
head() {
|
||||
return this.selectedTemplate ? head({
|
||||
title: `${this.$t('template.intro')}: ${this.selectedTemplate.name(this.glue)}`,
|
||||
banner: `banner${this.$route.path.replace(/\/$/, '')}.png`,
|
||||
banner: `api/banner${this.$route.path.replace(/\/$/, '')}.png`,
|
||||
}) : {};
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import {renderJson} from "../src/helpers";
|
||||
|
||||
const dbConnection = require('./db');
|
||||
const SQL = require('sql-template-strings');
|
||||
import authenticate from './authenticate';
|
||||
|
||||
|
||||
export default async function (req, res, next) {
|
||||
const db = await dbConnection();
|
||||
const user = authenticate(req);
|
||||
|
||||
if (!user || !user.authenticated || user.roles !== 'admin') {
|
||||
return renderJson(res, {error: 'unauthorised'}, 401);
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && req.url === '/users') {
|
||||
const users = await db.all(SQL`
|
||||
SELECT u.id, u.username, u.email, u.roles, p.locale
|
||||
FROM users u
|
||||
LEFT JOIN profiles p ON p.userId = u.id
|
||||
ORDER BY u.id DESC
|
||||
`);
|
||||
|
||||
const groupedUsers = {};
|
||||
for (let user of users) {
|
||||
if (groupedUsers[user.id] === undefined) {
|
||||
groupedUsers[user.id] = {
|
||||
...user,
|
||||
locale: undefined,
|
||||
profiles: user.locale ? [user.locale] : [],
|
||||
}
|
||||
} else {
|
||||
groupedUsers[user.id].profiles.push(user.locale);
|
||||
}
|
||||
}
|
||||
|
||||
return renderJson(res, groupedUsers);
|
||||
}
|
||||
|
||||
return renderJson(res, { error: 'notfound' }, 404);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import express from 'express';
|
||||
import authenticate from '../src/authenticate';
|
||||
import dbConnection from './db';
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
app.use(async function (req, res, next) {
|
||||
req.rawUser = authenticate(req);
|
||||
req.user = req.rawUser && req.rawUser.authenticated ? req.rawUser : null;
|
||||
req.admin = req.user && req.user.roles === 'admin';
|
||||
req.db = await dbConnection();
|
||||
next();
|
||||
})
|
||||
|
||||
app.use(require('./routes/banner').default);
|
||||
|
||||
app.use(require('./routes/user').default);
|
||||
app.use(require('./routes/profile').default);
|
||||
app.use(require('./routes/admin').default);
|
||||
|
||||
app.use(require('./routes/nouns').default);
|
||||
app.use(require('./routes/sources').default);
|
||||
|
||||
export default {
|
||||
path: '/api',
|
||||
handler: app,
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
const dbConnection = require('./db');
|
||||
require('dotenv').config({ path:__dirname + '/../.env' });
|
||||
const mailer = require('./mailer');
|
||||
const mailer = require('../src/mailer');
|
||||
|
||||
async function notify() {
|
||||
const db = await dbConnection();
|
||||
|
|
111
server/nouns.js
111
server/nouns.js
|
@ -1,111 +0,0 @@
|
|||
import {renderJson} from "../src/helpers";
|
||||
|
||||
const dbConnection = require('./db');
|
||||
const SQL = require('sql-template-strings');
|
||||
import { ulid } from 'ulid'
|
||||
import authenticate from './authenticate';
|
||||
|
||||
|
||||
|
||||
const getId = url => url.match(/\/([^/]+)$/)[1];
|
||||
|
||||
const approve = async (db, id) => {
|
||||
const {base_id} = await db.get(SQL`SELECT base_id FROM nouns WHERE id=${id}`);
|
||||
if (base_id) {
|
||||
await db.get(SQL`
|
||||
DELETE FROM nouns
|
||||
WHERE id = ${base_id}
|
||||
`);
|
||||
}
|
||||
await db.get(SQL`
|
||||
UPDATE nouns
|
||||
SET approved = 1, base_id = NULL
|
||||
WHERE id = ${id}
|
||||
`);
|
||||
}
|
||||
|
||||
const hide = async (db, id) => {
|
||||
await db.get(SQL`
|
||||
UPDATE nouns
|
||||
SET approved = 0
|
||||
WHERE id = ${id}
|
||||
`);
|
||||
}
|
||||
|
||||
const remove = async (db, id) => {
|
||||
await db.get(SQL`
|
||||
DELETE FROM nouns
|
||||
WHERE id = ${id}
|
||||
`);
|
||||
}
|
||||
|
||||
const trollWords = [
|
||||
'cipeusz',
|
||||
'feminazi',
|
||||
'bruksela',
|
||||
'zboczeń',
|
||||
];
|
||||
|
||||
const isTroll = (body) => {
|
||||
const jsonBody = JSON.stringify(body);
|
||||
for (let trollWord of trollWords) {
|
||||
if (jsonBody.indexOf(trollWord) > -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default async function (req, res, next) {
|
||||
const db = await dbConnection();
|
||||
const user = authenticate(req);
|
||||
const isAdmin = user && user.authenticated && user.roles === 'admin';
|
||||
|
||||
if (req.method === 'GET' && req.url.startsWith('/all/')) {
|
||||
const locale = req.url.substring(5);
|
||||
return renderJson(res, await db.all(SQL`
|
||||
SELECT * FROM nouns
|
||||
WHERE locale = ${locale}
|
||||
AND approved >= ${isAdmin ? 0 : 1}
|
||||
ORDER BY approved, masc
|
||||
`));
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url.startsWith('/submit/')) {
|
||||
const locale = req.url.substring(8);
|
||||
if (isAdmin || !isTroll(req.body)) {
|
||||
const id = ulid()
|
||||
await db.get(SQL`
|
||||
INSERT INTO nouns (id, masc, fem, neutr, mascPl, femPl, neutrPl, approved, base_id, locale)
|
||||
VALUES (
|
||||
${id},
|
||||
${req.body.masc.join('|')}, ${req.body.fem.join('|')}, ${req.body.neutr.join('|')},
|
||||
${req.body.mascPl.join('|')}, ${req.body.femPl.join('|')}, ${req.body.neutrPl.join('|')},
|
||||
0, ${req.body.base}, ${locale}
|
||||
)
|
||||
`);
|
||||
if (isAdmin) {
|
||||
await approve(db, id);
|
||||
}
|
||||
}
|
||||
return renderJson(res, 'ok');
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url.startsWith('/approve/') && isAdmin) {
|
||||
await approve(db, getId(req.url));
|
||||
return renderJson(res, 'ok');
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url.startsWith('/hide/') && isAdmin) {
|
||||
await hide(db, getId(req.url));
|
||||
return renderJson(res, 'ok');
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url.startsWith('/remove/') && isAdmin) {
|
||||
await remove(db, getId(req.url));
|
||||
return renderJson(res, 'ok');
|
||||
}
|
||||
|
||||
return renderJson(res, {error: 'Not found'}, 404);
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
const dbConnection = require('./db');
|
||||
const SQL = require('sql-template-strings');
|
||||
import {buildDict, renderJson} from "../src/helpers";
|
||||
import { ulid } from 'ulid'
|
||||
import authenticate from './authenticate';
|
||||
import md5 from 'js-md5';
|
||||
|
||||
const calcAge = birthday => {
|
||||
if (!birthday) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const birth = new Date(
|
||||
parseInt(birthday.substring(0, 4)),
|
||||
parseInt(birthday.substring(5, 7)) - 1,
|
||||
parseInt(birthday.substring(8, 10))
|
||||
);
|
||||
|
||||
const diff = now.getTime() - birth.getTime();
|
||||
|
||||
return parseInt(Math.floor(diff / 1000 / 60 / 60 / 24 / 365.25));
|
||||
}
|
||||
|
||||
const buildProfile = profile => {
|
||||
return {
|
||||
id: profile.id,
|
||||
userId: profile.userId,
|
||||
username: profile.username,
|
||||
emailHash: md5(profile.email),
|
||||
names: JSON.parse(profile.names),
|
||||
pronouns: JSON.parse(profile.pronouns),
|
||||
description: profile.description,
|
||||
age: calcAge(profile.birthday),
|
||||
links: JSON.parse(profile.links),
|
||||
flags: JSON.parse(profile.flags),
|
||||
words: JSON.parse(profile.words),
|
||||
};
|
||||
};
|
||||
|
||||
const fetchProfiles = async (db, res, username, self) => {
|
||||
const profiles = await db.all(SQL`
|
||||
SELECT profiles.*, users.username, users.email FROM profiles LEFT JOIN users on users.id == profiles.userId
|
||||
WHERE users.username = ${username}
|
||||
AND profiles.active = 1
|
||||
ORDER BY profiles.locale
|
||||
`);
|
||||
|
||||
return renderJson(res, buildDict(function* () {
|
||||
for (let profile of profiles) {
|
||||
yield [
|
||||
profile.locale,
|
||||
{
|
||||
...buildProfile(profile),
|
||||
birthday: self ? profile.birthday : undefined,
|
||||
}
|
||||
];
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function (req, res, next) {
|
||||
const db = await dbConnection();
|
||||
const user = authenticate(req);
|
||||
|
||||
if (req.method === 'GET' && req.url.startsWith('/get/')) {
|
||||
const username = req.url.substring(5);
|
||||
return await fetchProfiles(db, res, username, user && user.authenticated && user.username === username)
|
||||
}
|
||||
|
||||
if (!user || !user.authenticated) {
|
||||
return renderJson(res, {error: 'unauthorised'}, 401);
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url.startsWith('/save/')) {
|
||||
const locale = req.url.substring(6);
|
||||
const userId = (await db.get(SQL`SELECT id FROM users WHERE username = ${user.username}`)).id;
|
||||
|
||||
await db.get(SQL`DELETE FROM profiles WHERE userId = ${userId} AND locale = ${locale}`);
|
||||
await db.get(SQL`INSERT INTO profiles (id, userId, locale, names, pronouns, description, birthday, links, flags, words, active)
|
||||
VALUES (${ulid()}, ${userId}, ${locale}, ${JSON.stringify(req.body.names)}, ${JSON.stringify(req.body.pronouns)},
|
||||
${req.body.description}, ${req.body.birthday || null}, ${JSON.stringify(req.body.links)}, ${JSON.stringify(req.body.flags)},
|
||||
${JSON.stringify(req.body.words)}, 1
|
||||
)`);
|
||||
|
||||
return fetchProfiles(db, res, user.username, true);
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url.startsWith('/delete/')) {
|
||||
const locale = req.url.substring(8);
|
||||
const userId = (await db.get(SQL`SELECT id FROM users WHERE username = ${user.username}`)).id;
|
||||
|
||||
await db.get(SQL`DELETE FROM profiles WHERE userId = ${userId} AND locale = ${locale}`);
|
||||
|
||||
return fetchProfiles(db, res, user.username, true);
|
||||
}
|
||||
|
||||
return renderJson(res, { error: 'notfound' }, 404);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { Router } from 'express';
|
||||
import SQL from 'sql-template-strings';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/admin/users', async (req, res) => {
|
||||
if (!req.admin) {
|
||||
return res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
const users = await req.db.all(SQL`
|
||||
SELECT u.id, u.username, u.email, u.roles, p.locale
|
||||
FROM users u
|
||||
LEFT JOIN profiles p ON p.userId = u.id
|
||||
ORDER BY u.id DESC
|
||||
`);
|
||||
|
||||
const groupedUsers = {};
|
||||
for (let user of users) {
|
||||
if (groupedUsers[user.id] === undefined) {
|
||||
groupedUsers[user.id] = {
|
||||
...user,
|
||||
locale: undefined,
|
||||
profiles: user.locale ? [user.locale] : [],
|
||||
}
|
||||
} else {
|
||||
groupedUsers[user.id].profiles.push(user.locale);
|
||||
}
|
||||
}
|
||||
|
||||
return res.json(groupedUsers);
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,11 +1,10 @@
|
|||
import { buildTemplate, parseTemplates } from "../src/buildTemplate";
|
||||
import { createCanvas, registerFont, loadImage } from 'canvas';
|
||||
import { loadTsv } from './tsv';
|
||||
import translations from '../server/translations';
|
||||
import {gravatar, renderImage, renderText} from "../src/helpers";
|
||||
const dbConnection = require('./db');
|
||||
const SQL = require('sql-template-strings');
|
||||
|
||||
import { Router } from 'express';
|
||||
import SQL from 'sql-template-strings';
|
||||
import {createCanvas, loadImage, registerFont} from "canvas";
|
||||
import translations from "../translations";
|
||||
import {gravatar} from "../../src/helpers";
|
||||
import {buildTemplate, parseTemplates} from "../../src/buildTemplate";
|
||||
import {loadTsv} from "../../src/tsv";
|
||||
|
||||
const drawCircle = (context, image, x, y, size) => {
|
||||
context.save();
|
||||
|
@ -23,14 +22,9 @@ const drawCircle = (context, image, x, y, size) => {
|
|||
context.restore();
|
||||
}
|
||||
|
||||
const router = Router();
|
||||
|
||||
export default async function (req, res, next) {
|
||||
if (req.url.substr(req.url.length - 4) !== '.png') {
|
||||
return renderText(res, 'Not found', 404);
|
||||
}
|
||||
|
||||
const templateName = decodeURIComponent(req.url.substr(1, req.url.length - 5));
|
||||
|
||||
router.get('/banner/:templateName.png', async (req, res) => {
|
||||
const width = 1200
|
||||
const height = 600
|
||||
const mime = 'image/png';
|
||||
|
@ -55,12 +49,11 @@ export default async function (req, res, next) {
|
|||
context.fillText(translations.title, width / leftRatio + imageSize / 1.5, height / 2 + 48);
|
||||
}
|
||||
|
||||
if (templateName.startsWith('@')) {
|
||||
const db = await dbConnection();
|
||||
const user = await db.get(SQL`SELECT username, email FROM users WHERE username=${templateName.substring(1)}`);
|
||||
if (req.params.templateName.startsWith('@')) {
|
||||
const user = await req.db.get(SQL`SELECT username, email FROM users WHERE username=${req.params.templateName.substring(1)}`);
|
||||
if (!user) {
|
||||
await fallback();
|
||||
return renderImage(res, canvas, mime);
|
||||
return res.set('content-type', mime).send(canvas.toBuffer(mime));
|
||||
}
|
||||
|
||||
const avatar = await loadImage(gravatar(user, imageSize));
|
||||
|
@ -78,28 +71,30 @@ export default async function (req, res, next) {
|
|||
context.drawImage(logo, width / leftRatio + imageSize, height / 2 + logoSize - 4, logoSize, logoSize / 1.25)
|
||||
context.fillText(translations.title, width / leftRatio + imageSize + 36, height / 2 + 48);
|
||||
|
||||
return renderImage(res, canvas, mime);
|
||||
return res.set('content-type', mime).send(canvas.toBuffer(mime));
|
||||
}
|
||||
|
||||
const template = buildTemplate(
|
||||
parseTemplates(loadTsv(__dirname + '/../data/templates/templates.tsv')),
|
||||
templateName,
|
||||
parseTemplates(loadTsv(__dirname + '/../../data/templates/templates.tsv')),
|
||||
req.params.templateName,
|
||||
);
|
||||
|
||||
const logo = await loadImage('node_modules/@fortawesome/fontawesome-pro/svgs/light/tags.svg');
|
||||
|
||||
if (!template && templateName !== 'dowolne') {
|
||||
if (!template && req.params.templateName !== 'dowolne') { // TODO
|
||||
await fallback();
|
||||
return renderImage(res, canvas, mime);
|
||||
return res.set('content-type', mime).send(canvas.toBuffer(mime));
|
||||
}
|
||||
|
||||
context.drawImage(logo, width / leftRatio - imageSize / 2, height / 2 - imageSize / 1.25 / 2, imageSize, imageSize / 1.25)
|
||||
context.font = 'regular 48pt Quicksand'
|
||||
context.fillText(translations.template.intro + ':', width / leftRatio + imageSize / 1.5, height / 2 - 36)
|
||||
|
||||
const templateNameOptions = templateName === 'dowolne' ? ['dowolne'] : template.nameOptions();
|
||||
const templateNameOptions = req.params.templateName === 'dowolne' ? ['dowolne'] : template.nameOptions();
|
||||
context.font = `bold ${templateNameOptions.length <= 2 ? '70' : '36'}pt Quicksand`
|
||||
context.fillText(templateNameOptions.join('\n'), width / leftRatio + imageSize / 1.5, height / 2 + (templateNameOptions.length <= 2 ? 72 : 24))
|
||||
|
||||
return renderImage(res, canvas, mime);
|
||||
}
|
||||
return res.set('content-type', mime).send(canvas.toBuffer(mime));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,95 @@
|
|||
import { Router } from 'express';
|
||||
import SQL from 'sql-template-strings';
|
||||
import {ulid} from "ulid";
|
||||
|
||||
const isTroll = (body) => {
|
||||
return ['cipeusz', 'feminazi', 'bruksela', 'zboczeń'].some(t => body.indexOf(t) > -1);
|
||||
}
|
||||
|
||||
const approve = async (db, id) => {
|
||||
const { base_id } = await db.get(SQL`SELECT base_id FROM nouns WHERE id=${id}`);
|
||||
if (base_id) {
|
||||
await db.get(SQL`
|
||||
DELETE FROM nouns
|
||||
WHERE id = ${base_id}
|
||||
`);
|
||||
}
|
||||
await db.get(SQL`
|
||||
UPDATE nouns
|
||||
SET approved = 1, base_id = NULL
|
||||
WHERE id = ${id}
|
||||
`);
|
||||
}
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/nouns/all/:locale', async (req, res) => {
|
||||
return res.json(await req.db.all(SQL`
|
||||
SELECT * FROM nouns
|
||||
WHERE locale = ${req.params.locale}
|
||||
AND approved >= ${req.admin ? 0 : 1}
|
||||
ORDER BY approved, masc
|
||||
`));
|
||||
});
|
||||
|
||||
router.post('/nouns/submit/:locale', async (req, res) => {
|
||||
if (!(req.user && $req.user.admin) && isTroll(JSON.stringify(body))) {
|
||||
return res.json('ok');
|
||||
}
|
||||
|
||||
const id = ulid();
|
||||
await req.db.get(SQL`
|
||||
INSERT INTO nouns (id, masc, fem, neutr, mascPl, femPl, neutrPl, approved, base_id, locale)
|
||||
VALUES (
|
||||
${id},
|
||||
${req.body.masc.join('|')}, ${req.body.fem.join('|')}, ${req.body.neutr.join('|')},
|
||||
${req.body.mascPl.join('|')}, ${req.body.femPl.join('|')}, ${req.body.neutrPl.join('|')},
|
||||
0, ${req.body.base}, ${locale}
|
||||
)
|
||||
`);
|
||||
|
||||
if (req.admin) {
|
||||
await approve(req.db, id);
|
||||
}
|
||||
|
||||
return res.json('ok');
|
||||
});
|
||||
|
||||
router.post('/nouns/hide/:id', async (req, res) => {
|
||||
if (!req.admin) {
|
||||
res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
await req.db.get(SQL`
|
||||
UPDATE nouns
|
||||
SET approved = 0
|
||||
WHERE id = ${req.params.id}
|
||||
`);
|
||||
|
||||
return res.json('ok');
|
||||
});
|
||||
|
||||
router.post('/nouns/approve/:id', async (req, res) => {
|
||||
if (!req.admin) {
|
||||
res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
await approve(req.db, req.params.id);
|
||||
|
||||
return res.json('ok');
|
||||
});
|
||||
|
||||
router.post('/nouns/remove/:id', async (req, res) => {
|
||||
if (!req.admin) {
|
||||
res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
await req.db.get(SQL`
|
||||
DELETE FROM nouns
|
||||
WHERE id = ${req.params.id}
|
||||
`);
|
||||
|
||||
return res.json('ok');
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,92 @@
|
|||
import { Router } from 'express';
|
||||
import SQL from 'sql-template-strings';
|
||||
import md5 from "js-md5";
|
||||
import {buildDict} from "../../src/helpers";
|
||||
import {ulid} from "ulid";
|
||||
|
||||
const calcAge = birthday => {
|
||||
if (!birthday) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const birth = new Date(
|
||||
parseInt(birthday.substring(0, 4)),
|
||||
parseInt(birthday.substring(5, 7)) - 1,
|
||||
parseInt(birthday.substring(8, 10))
|
||||
);
|
||||
|
||||
const diff = now.getTime() - birth.getTime();
|
||||
|
||||
return parseInt(Math.floor(diff / 1000 / 60 / 60 / 24 / 365.25));
|
||||
}
|
||||
|
||||
const buildProfile = profile => {
|
||||
return {
|
||||
id: profile.id,
|
||||
userId: profile.userId,
|
||||
username: profile.username,
|
||||
emailHash: md5(profile.email),
|
||||
names: JSON.parse(profile.names),
|
||||
pronouns: JSON.parse(profile.pronouns),
|
||||
description: profile.description,
|
||||
age: calcAge(profile.birthday),
|
||||
links: JSON.parse(profile.links),
|
||||
flags: JSON.parse(profile.flags),
|
||||
words: JSON.parse(profile.words),
|
||||
};
|
||||
};
|
||||
|
||||
const fetchProfiles = async (db, username, self) => {
|
||||
const profiles = await db.all(SQL`
|
||||
SELECT profiles.*, users.username, users.email FROM profiles LEFT JOIN users on users.id == profiles.userId
|
||||
WHERE users.username = ${username}
|
||||
AND profiles.active = 1
|
||||
ORDER BY profiles.locale
|
||||
`);
|
||||
|
||||
return buildDict(function* () {
|
||||
for (let profile of profiles) {
|
||||
yield [
|
||||
profile.locale,
|
||||
{
|
||||
...buildProfile(profile),
|
||||
birthday: self ? profile.birthday : undefined,
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/profile/get/:username', async (req, res) => {
|
||||
return res.json(await fetchProfiles(req.db, req.params.username, req.user && req.user.username === req.params.username))
|
||||
});
|
||||
|
||||
router.post('/profile/save/:locale', async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
const userId = (await req.db.get(SQL`SELECT id FROM users WHERE username = ${req.user.username}`)).id;
|
||||
|
||||
await req.db.get(SQL`DELETE FROM profiles WHERE userId = ${userId} AND locale = ${req.params.locale}`);
|
||||
await req.db.get(SQL`INSERT INTO profiles (id, userId, locale, names, pronouns, description, birthday, links, flags, words, active)
|
||||
VALUES (${ulid()}, ${userId}, ${req.params.locale}, ${JSON.stringify(req.body.names)}, ${JSON.stringify(req.body.pronouns)},
|
||||
${req.body.description}, ${req.body.birthday || null}, ${JSON.stringify(req.body.links)}, ${JSON.stringify(req.body.flags)},
|
||||
${JSON.stringify(req.body.words)}, 1
|
||||
)`);
|
||||
|
||||
return res.json(await fetchProfiles(req.db, req.user.username, true));
|
||||
});
|
||||
|
||||
router.post('/profile/delete/:locale', async (req, res) => {
|
||||
const userId = (await req.db.get(SQL`SELECT id FROM users WHERE username = ${req.user.username}`)).id;
|
||||
|
||||
await req.db.get(SQL`DELETE FROM profiles WHERE userId = ${userId} AND locale = ${req.params.locale}`);
|
||||
|
||||
return res.json(await fetchProfiles(req.db, req.user.username, true));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,31 @@
|
|||
import { Router } from 'express';
|
||||
import mailer from "../../src/mailer";
|
||||
|
||||
const buildEmail = (data, user) => {
|
||||
const human = [
|
||||
`<li><strong>user:</strong> ${user ? user.username : ''}</li>`,
|
||||
`<li><strong>templates:</strong> ${data.templates}</li>`,
|
||||
];
|
||||
const tsv = ['???'];
|
||||
|
||||
for (let field of ['type','author','title','extra','year','fragments','comment','link']) {
|
||||
human.push(`<li><strong>${field}:</strong> ${field === 'fragments' ? `<pre>${data[field]}</pre>`: data[field]}</li>`);
|
||||
tsv.push(field === 'fragments' ? (data[field].join('@').replace(/\n/g, '|')) : data[field]);
|
||||
}
|
||||
|
||||
return `<ul>${human.join('')}</ul><pre>${tsv.join('\t')}</pre>`;
|
||||
}
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/sources/submit/:locale', async (req, res) => {
|
||||
const emailBody = buildEmail(req.body, req.user);
|
||||
|
||||
for (let admin of process.env.MAILER_ADMINS.split(',')) {
|
||||
mailer(admin, `[Pronouns][${req.params.locale}] Source submission`, undefined, emailBody);
|
||||
}
|
||||
|
||||
return res.json({ result: 'ok' });
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,241 @@
|
|||
import { Router } from 'express';
|
||||
import SQL from 'sql-template-strings';
|
||||
import {ulid} from "ulid";
|
||||
import {makeId} from "../../src/helpers";
|
||||
import translations from "../translations";
|
||||
import jwt from "../../src/jwt";
|
||||
import mailer from "../../src/mailer";
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';
|
||||
|
||||
const normalise = s => s.trim().toLowerCase();
|
||||
|
||||
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 defaultUsername = async (db, email) => {
|
||||
const base = email.substring(0, email.indexOf('@'))
|
||||
.padEnd(4, '0')
|
||||
.substring(0, 12)
|
||||
.replace(new RegExp(`[^${USERNAME_CHARS}]`, 'g'), '_');
|
||||
|
||||
let c = 0;
|
||||
while (true) {
|
||||
let proposal = base + (c || '');
|
||||
let dbUser = await db.get(SQL`SELECT id FROM users WHERE lower(trim(username)) = ${normalise(proposal)}`);
|
||||
if (!dbUser) {
|
||||
return proposal;
|
||||
}
|
||||
c++;
|
||||
}
|
||||
}
|
||||
|
||||
const issueAuthentication = async (db, user) => {
|
||||
let dbUser = await db.get(SQL`SELECT * FROM users WHERE email = ${normalise(user.email)}`);
|
||||
if (!dbUser) {
|
||||
dbUser = {
|
||||
id: ulid(),
|
||||
username: await defaultUsername(db, user.email),
|
||||
email: normalise(user.email),
|
||||
roles: 'user',
|
||||
avatarSource: null,
|
||||
}
|
||||
await db.get(SQL`INSERT INTO users(id, username, email, roles, avatarSource)
|
||||
VALUES (${dbUser.id}, ${dbUser.username}, ${dbUser.email}, ${dbUser.roles}, ${dbUser.avatarSource})`)
|
||||
}
|
||||
|
||||
return {
|
||||
token: jwt.sign({
|
||||
...dbUser,
|
||||
authenticated: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
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 router = Router();
|
||||
|
||||
router.post('/user/init', async (req, res) => {
|
||||
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 {
|
||||
user = await req.db.get(SQL`SELECT * FROM users WHERE lower(trim(username)) = ${normalise(usernameOrEmail)}`);
|
||||
}
|
||||
|
||||
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'),
|
||||
}
|
||||
|
||||
const codeKey = await saveAuthenticator(req.db, 'email', user, payload, 15);
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
return res.json({
|
||||
token: jwt.sign({...payload, code: null, codeKey}, '15m'),
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/user/validate', async (req, res) => {
|
||||
if (!req.rawUser || !req.rawUser.codeKey) {
|
||||
return res.json({error: 'user.tokenExpired'});
|
||||
}
|
||||
|
||||
const authenticator = await findAuthenticator(req.db, req.rawUser.codeKey, 'email');
|
||||
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);
|
||||
|
||||
return res.json(await issueAuthentication(req.db, req.rawUser));
|
||||
});
|
||||
|
||||
router.post('/user/change-username', async (req, res) => {
|
||||
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}]+$`))) {
|
||||
return { error: 'user.account.changeUsername.invalid' }
|
||||
}
|
||||
|
||||
const dbUser = await req.db.get(SQL`SELECT * FROM users WHERE lower(trim(username)) = ${normalise(req.body.username)}`);
|
||||
if (dbUser) {
|
||||
return res.json({ error: 'user.account.changeUsername.taken' })
|
||||
}
|
||||
|
||||
await req.db.get(SQL`UPDATE users SET username = ${req.body.username} WHERE email = ${normalise(req.user.email)}`);
|
||||
|
||||
return res.json(await issueAuthentication(req.db, req.user));
|
||||
});
|
||||
|
||||
router.post('/user/change-email', async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
if (!validateEmail(req.user.email)) {
|
||||
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) {
|
||||
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);
|
||||
|
||||
mailer(
|
||||
payload.to,
|
||||
`[${translations.title}] ${translations.user.login.email.subject.replace('%code%', payload.code)}`,
|
||||
translations.user.login.email.content.replace('%code%', payload.code),
|
||||
)
|
||||
|
||||
return res.json({ authId });
|
||||
}
|
||||
|
||||
const authenticator = await findAuthenticator(req.db, req.body.authId, 'changeEmail');
|
||||
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);
|
||||
|
||||
await req.db.get(SQL`UPDATE users SET email = ${authenticator.payload.to} WHERE email = ${normalise(req.user.email)}`);
|
||||
req.user.email = authenticator.payload.to;
|
||||
|
||||
return res.json(await issueAuthentication(req.db, req.user));
|
||||
});
|
||||
|
||||
router.post('/user/delete', async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
const userId = (await req.db.get(SQL`SELECT id FROM users WHERE username = ${req.user.username}`)).id;
|
||||
if (!userId) {
|
||||
return res.json(false);
|
||||
}
|
||||
|
||||
await req.db.get(SQL`DELETE FROM profiles WHERE userId = ${userId}`)
|
||||
await req.db.get(SQL`DELETE FROM authenticators WHERE userId = ${userId}`)
|
||||
await req.db.get(SQL`DELETE FROM users WHERE id = ${userId}`)
|
||||
|
||||
return res.json(true);
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,38 +0,0 @@
|
|||
const dbConnection = require('./db');
|
||||
import {renderJson} from "../src/helpers";
|
||||
import authenticate from './authenticate';
|
||||
const mailer = require('./mailer');
|
||||
|
||||
|
||||
const buildEmail = (data, user) => {
|
||||
const human = [
|
||||
`<li><strong>user:</strong> ${user ? user.username : ''}</li>`,
|
||||
`<li><strong>templates:</strong> ${data.templates}</li>`,
|
||||
];
|
||||
const tsv = ['???'];
|
||||
|
||||
for (let field of ['type','author','title','extra','year','fragments','comment','link']) {
|
||||
human.push(`<li><strong>${field}:</strong> ${data[field]}</li>`);
|
||||
tsv.push(data[field]);
|
||||
}
|
||||
|
||||
return `<ul>${human.join('')}</ul><pre>${tsv.join('\t')}</pre>`;
|
||||
}
|
||||
|
||||
export default async function (req, res, next) {
|
||||
const db = await dbConnection();
|
||||
const user = authenticate(req);
|
||||
|
||||
if (req.method === 'POST' && req.url.startsWith('/submit/')) {
|
||||
const locale = req.url.substring(8);
|
||||
|
||||
const emailBody = buildEmail(req.body, user);
|
||||
|
||||
for (let admin of process.env.MAILER_ADMINS.split(',')) {
|
||||
mailer(admin, `[Pronouns][${locale}] Source submission`, undefined, emailBody);
|
||||
}
|
||||
return renderJson(res, { result: 'ok' });
|
||||
}
|
||||
|
||||
return renderJson(res, { error: 'notfound' }, 404);
|
||||
}
|
250
server/user.js
250
server/user.js
|
@ -1,250 +0,0 @@
|
|||
import jwt from './jwt';
|
||||
import {makeId, renderJson} from '../src/helpers';
|
||||
const dbConnection = require('./db');
|
||||
const SQL = require('sql-template-strings');
|
||||
import { ulid } from 'ulid';
|
||||
import translations from "./translations";
|
||||
const mailer = require('./mailer');
|
||||
import authenticate from './authenticate';
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';
|
||||
|
||||
const normalise = s => s.trim().toLowerCase();
|
||||
|
||||
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;
|
||||
let isTest = false;
|
||||
|
||||
if (process.env.NODE_ENV === 'development' && usernameOrEmail.endsWith('+')) {
|
||||
isTest = true;
|
||||
usernameOrEmail = usernameOrEmail.substring(0, usernameOrEmail.length - 1);
|
||||
}
|
||||
|
||||
if (isEmail) {
|
||||
user = await db.get(SQL`SELECT * FROM users WHERE email = ${normalise(usernameOrEmail)}`);
|
||||
} else {
|
||||
user = await db.get(SQL`SELECT * FROM users WHERE lower(trim(username)) = ${normalise(usernameOrEmail)}`);
|
||||
}
|
||||
|
||||
if (!user && !isEmail) {
|
||||
return {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'),
|
||||
}
|
||||
|
||||
const codeKey = await saveAuthenticator(db, 'email', user, payload, 15);
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
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'};
|
||||
}
|
||||
|
||||
if (authenticator.payload.code !== normalise(code)) {
|
||||
return {error: 'user.code.invalid'};
|
||||
}
|
||||
|
||||
await invalidateAuthenticator(db, authenticator);
|
||||
|
||||
return await issueAuthentication(db, user);
|
||||
}
|
||||
|
||||
const defaultUsername = async (db, email) => {
|
||||
const base = email.substring(0, email.indexOf('@'))
|
||||
.padEnd(4, '0')
|
||||
.substring(0, 12)
|
||||
.replace(new RegExp(`[^${USERNAME_CHARS}]`, 'g'), '_');
|
||||
|
||||
let c = 0;
|
||||
while (true) {
|
||||
let proposal = base + (c || '');
|
||||
let dbUser = await db.get(SQL`SELECT id FROM users WHERE lower(trim(username)) = ${normalise(proposal)}`);
|
||||
if (!dbUser) {
|
||||
return proposal;
|
||||
}
|
||||
c++;
|
||||
}
|
||||
}
|
||||
|
||||
const issueAuthentication = async (db, user) => {
|
||||
let dbUser = await db.get(SQL`SELECT * FROM users WHERE email = ${normalise(user.email)}`);
|
||||
if (!dbUser) {
|
||||
dbUser = {
|
||||
id: ulid(),
|
||||
username: await defaultUsername(db, user.email),
|
||||
email: normalise(user.email),
|
||||
roles: 'user',
|
||||
avatarSource: null,
|
||||
}
|
||||
await db.get(SQL`INSERT INTO users(id, username, email, roles, avatarSource)
|
||||
VALUES (${dbUser.id}, ${dbUser.username}, ${dbUser.email}, ${dbUser.roles}, ${dbUser.avatarSource})`)
|
||||
}
|
||||
|
||||
return {
|
||||
token: jwt.sign({
|
||||
...dbUser,
|
||||
authenticated: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
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' }
|
||||
}
|
||||
|
||||
const dbUser = await db.get(SQL`SELECT * FROM users WHERE lower(trim(username)) = ${normalise(username)}`);
|
||||
if (dbUser) {
|
||||
return { error: 'user.account.changeUsername.taken' }
|
||||
}
|
||||
|
||||
await db.get(SQL`UPDATE users SET username = ${username} WHERE email = ${normalise(user.email)}`);
|
||||
|
||||
return await issueAuthentication(db, user);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export default async function (req, res, next) {
|
||||
const db = await dbConnection();
|
||||
const user = authenticate(req);
|
||||
|
||||
if (req.method === 'POST' && req.url === '/init' && req.body.usernameOrEmail) {
|
||||
return renderJson(res, await init(db, req.body.usernameOrEmail));
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url === '/validate' && req.body.code) {
|
||||
return renderJson(res, await validate(db, user, req.body.code));
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url === '/change-username' && user && user.authenticated && req.body.username) {
|
||||
return renderJson(res, await changeUsername(db, user, req.body.username));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url === '/delete' && user && user.authenticated) {
|
||||
return renderJson(res, await removeAccount(db, user));
|
||||
}
|
||||
|
||||
return renderJson(res, {error: 'Not found'}, 404);
|
||||
}
|
|
@ -21,7 +21,7 @@ export const head = ({title, description, banner}) => {
|
|||
const meta = { meta: [] };
|
||||
|
||||
if (title) {
|
||||
title += ' • Zaimki.pl';
|
||||
title += ' • ' + process.env.TITLE;
|
||||
meta.title = title;
|
||||
meta.meta.push({ hid: 'og:title', property: 'og:title', content: title });
|
||||
meta.meta.push({ hid: 'twitter:title', property: 'twitter:title', content: title });
|
||||
|
@ -76,37 +76,6 @@ export const makeId = (length, characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi
|
|||
return result;
|
||||
}
|
||||
|
||||
export const parseQuery = (queryString) => {
|
||||
const query = {};
|
||||
const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
let pair = pairs[i].split('=');
|
||||
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
export const renderText = (res, content, status = 200) => {
|
||||
res.statusCode = status;
|
||||
res.setHeader('content-type', 'application/json');
|
||||
res.write(JSON.stringify(content));
|
||||
res.end();
|
||||
}
|
||||
|
||||
export const renderJson = (res, content, status = 200) => {
|
||||
res.statusCode = status;
|
||||
res.setHeader('content-type', 'application/json');
|
||||
res.write(JSON.stringify(content));
|
||||
res.end();
|
||||
}
|
||||
|
||||
export const renderImage = (res, canvas, mime, status = 200) => {
|
||||
res.statusCode = status;
|
||||
res.setHeader('content-type', mime);
|
||||
res.write(canvas.toBuffer(mime));
|
||||
res.end();
|
||||
}
|
||||
|
||||
export const gravatar = (user, size = 128) => {
|
||||
const fallback = `https://avi.avris.it/${size}/${Base64.encode(user.username).replace(/\+/g, '-').replace(/\//g, '_')}.png`;
|
||||
|
||||
|
|
|
@ -3740,7 +3740,7 @@ expand-brackets@^2.1.4:
|
|||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
express@^4.16.3:
|
||||
express@^4.16.3, express@^4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
|
||||
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
|
||||
|
|
Reference in New Issue