mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-24 05:05:20 -04:00
#87 move backend to express
This commit is contained in:
parent
7cd6072787
commit
deff979215
@ -36,11 +36,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async removeProfile() {
|
async removeProfile(locale) {
|
||||||
await this.$confirm(this.$t('profile.deleteConfirm'), 'danger');
|
await this.$confirm(this.$t('profile.deleteConfirm'), 'danger');
|
||||||
|
|
||||||
this.deleting = true;
|
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.deleting = false;
|
||||||
this.$emit('update', response);
|
this.$emit('update', response);
|
||||||
},
|
},
|
||||||
|
@ -86,6 +86,7 @@ export default {
|
|||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
BASE_URL: process.env.BASE_URL,
|
BASE_URL: process.env.BASE_URL,
|
||||||
|
TITLE: title,
|
||||||
PUBLIC_KEY: fs.readFileSync(__dirname + '/keys/public.pem').toString(),
|
PUBLIC_KEY: fs.readFileSync(__dirname + '/keys/public.pem').toString(),
|
||||||
LOCALE: config.locale,
|
LOCALE: config.locale,
|
||||||
FLAGS: buildDict(function *() {
|
FLAGS: buildDict(function *() {
|
||||||
@ -102,15 +103,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
serverMiddleware: {
|
serverMiddleware: ['~/server/index.js'],
|
||||||
'/': 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',
|
|
||||||
},
|
|
||||||
axios: {
|
axios: {
|
||||||
baseURL: process.env.BASE_URL + '/api',
|
baseURL: process.env.BASE_URL + '/api',
|
||||||
},
|
},
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"canvas": "^2.6.1",
|
"canvas": "^2.6.1",
|
||||||
"cookie-universal-nuxt": "^2.1.4",
|
"cookie-universal-nuxt": "^2.1.4",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
"js-base64": "^3.5.2",
|
"js-base64": "^3.5.2",
|
||||||
"js-md5": "^0.7.3",
|
"js-md5": "^0.7.3",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
head() {
|
head() {
|
||||||
return head({
|
return head({
|
||||||
title: `${this.$t('template.intro')}: ${this.$t('template.any.short')}`,
|
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: {
|
methods: {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<nuxt-link v-if="$user() && $user().username === username" :to="`/${config.profile.editorRoute}`"
|
<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"/>
|
<Icon v="edit"/>
|
||||||
<T>profile.edit</T>
|
<T>profile.edit</T>
|
||||||
@ -158,7 +158,7 @@
|
|||||||
head() {
|
head() {
|
||||||
return head({
|
return head({
|
||||||
title: `@${this.username}`,
|
title: `@${this.username}`,
|
||||||
banner: `banner/@${this.username}.png`,
|
banner: `api/banner/@${this.username}.png`,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@
|
|||||||
head() {
|
head() {
|
||||||
return this.selectedTemplate ? head({
|
return this.selectedTemplate ? head({
|
||||||
title: `${this.$t('template.intro')}: ${this.selectedTemplate.name(this.glue)}`,
|
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: {
|
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);
|
|
||||||
}
|
|
30
server/index.js
Normal file
30
server/index.js
Normal file
@ -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');
|
const dbConnection = require('./db');
|
||||||
require('dotenv').config({ path:__dirname + '/../.env' });
|
require('dotenv').config({ path:__dirname + '/../.env' });
|
||||||
const mailer = require('./mailer');
|
const mailer = require('../src/mailer');
|
||||||
|
|
||||||
async function notify() {
|
async function notify() {
|
||||||
const db = await dbConnection();
|
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);
|
|
||||||
}
|
|
34
server/routes/admin.js
Normal file
34
server/routes/admin.js
Normal file
@ -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 { Router } from 'express';
|
||||||
import { createCanvas, registerFont, loadImage } from 'canvas';
|
import SQL from 'sql-template-strings';
|
||||||
import { loadTsv } from './tsv';
|
import {createCanvas, loadImage, registerFont} from "canvas";
|
||||||
import translations from '../server/translations';
|
import translations from "../translations";
|
||||||
import {gravatar, renderImage, renderText} from "../src/helpers";
|
import {gravatar} from "../../src/helpers";
|
||||||
const dbConnection = require('./db');
|
import {buildTemplate, parseTemplates} from "../../src/buildTemplate";
|
||||||
const SQL = require('sql-template-strings');
|
import {loadTsv} from "../../src/tsv";
|
||||||
|
|
||||||
|
|
||||||
const drawCircle = (context, image, x, y, size) => {
|
const drawCircle = (context, image, x, y, size) => {
|
||||||
context.save();
|
context.save();
|
||||||
@ -23,14 +22,9 @@ const drawCircle = (context, image, x, y, size) => {
|
|||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
export default async function (req, res, next) {
|
router.get('/banner/:templateName.png', async (req, res) => {
|
||||||
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));
|
|
||||||
|
|
||||||
const width = 1200
|
const width = 1200
|
||||||
const height = 600
|
const height = 600
|
||||||
const mime = 'image/png';
|
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);
|
context.fillText(translations.title, width / leftRatio + imageSize / 1.5, height / 2 + 48);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (templateName.startsWith('@')) {
|
if (req.params.templateName.startsWith('@')) {
|
||||||
const db = await dbConnection();
|
const user = await req.db.get(SQL`SELECT username, email FROM users WHERE username=${req.params.templateName.substring(1)}`);
|
||||||
const user = await db.get(SQL`SELECT username, email FROM users WHERE username=${templateName.substring(1)}`);
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
await fallback();
|
await fallback();
|
||||||
return renderImage(res, canvas, mime);
|
return res.set('content-type', mime).send(canvas.toBuffer(mime));
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatar = await loadImage(gravatar(user, imageSize));
|
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.drawImage(logo, width / leftRatio + imageSize, height / 2 + logoSize - 4, logoSize, logoSize / 1.25)
|
||||||
context.fillText(translations.title, width / leftRatio + imageSize + 36, height / 2 + 48);
|
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(
|
const template = buildTemplate(
|
||||||
parseTemplates(loadTsv(__dirname + '/../data/templates/templates.tsv')),
|
parseTemplates(loadTsv(__dirname + '/../../data/templates/templates.tsv')),
|
||||||
templateName,
|
req.params.templateName,
|
||||||
);
|
);
|
||||||
|
|
||||||
const logo = await loadImage('node_modules/@fortawesome/fontawesome-pro/svgs/light/tags.svg');
|
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();
|
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.drawImage(logo, width / leftRatio - imageSize / 2, height / 2 - imageSize / 1.25 / 2, imageSize, imageSize / 1.25)
|
||||||
context.font = 'regular 48pt Quicksand'
|
context.font = 'regular 48pt Quicksand'
|
||||||
context.fillText(translations.template.intro + ':', width / leftRatio + imageSize / 1.5, height / 2 - 36)
|
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.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))
|
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;
|
95
server/routes/nouns.js
Normal file
95
server/routes/nouns.js
Normal file
@ -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;
|
92
server/routes/profile.js
Normal file
92
server/routes/profile.js
Normal file
@ -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;
|
31
server/routes/sources.js
Normal file
31
server/routes/sources.js
Normal file
@ -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;
|
241
server/routes/user.js
Normal file
241
server/routes/user.js
Normal file
@ -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: [] };
|
const meta = { meta: [] };
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
title += ' • Zaimki.pl';
|
title += ' • ' + process.env.TITLE;
|
||||||
meta.title = title;
|
meta.title = title;
|
||||||
meta.meta.push({ hid: 'og:title', property: 'og:title', content: title });
|
meta.meta.push({ hid: 'og:title', property: 'og:title', content: title });
|
||||||
meta.meta.push({ hid: 'twitter:title', property: 'twitter: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;
|
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) => {
|
export const gravatar = (user, size = 128) => {
|
||||||
const fallback = `https://avi.avris.it/${size}/${Base64.encode(user.username).replace(/\+/g, '-').replace(/\//g, '_')}.png`;
|
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"
|
snapdragon "^0.8.1"
|
||||||
to-regex "^3.0.1"
|
to-regex "^3.0.1"
|
||||||
|
|
||||||
express@^4.16.3:
|
express@^4.16.3, express@^4.17.1:
|
||||||
version "4.17.1"
|
version "4.17.1"
|
||||||
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
|
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
|
||||||
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
|
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user