diff --git a/.env.dist b/.env.dist
index 22a6833b..4b437848 100644
--- a/.env.dist
+++ b/.env.dist
@@ -1,5 +1,4 @@
BASE_URL=http://localhost:3000
-SECRET=secret
MAILER_HOST=
MAILER_PORT=
diff --git a/.gitignore b/.gitignore
index a8d7aa7f..ddf7ed3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@
/daemonise.json
/daemonise.log
+/keys
+
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
diff --git a/Makefile b/Makefile
index 079bf5c9..dd73e743 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,11 @@
+include .env.dist
+-include .env
+
+KEYS_DIR=./keys
+
install:
+ -cp -n .env.dist .env
+ if [ ! -d "${KEYS_DIR}" ]; then mkdir -p ${KEYS_DIR}; openssl genrsa -out ${KEYS_DIR}/private.pem 2048; openssl rsa -in ${KEYS_DIR}/private.pem -outform PEM -pubout -out ${KEYS_DIR}/public.pem; fi
yarn
node server/migrate.js
diff --git a/README.md b/README.md
index 4f253cbf..108969f5 100644
--- a/README.md
+++ b/README.md
@@ -4,27 +4,20 @@
```bash
# install dependencies
-$ yarn install
+$ make install
# configure environment
-$ cp .env.dist .env
$ nano .env
$ make switch LANG=pl
-$ node server/initDb.js
# serve with hot reload at localhost:3000
-$ yarn dev
+$ make run
# build for production and launch server
-$ yarn build
-$ yarn start
-
-# generate static project
-$ yarn generate
+$ make deploy
+$ nuxt start
```
-For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org).
-
## Copyright
* **Author:** Andrea [(Avris.it)](https://avris.it)
diff --git a/components/Account.vue b/components/Account.vue
new file mode 100644
index 00000000..e65232ff
--- /dev/null
+++ b/components/Account.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
user.account.changeEmail.header
+
{{ email }}
+
+
+
+ user.account.admin
+
+
+
+
+
+
+
diff --git a/components/Alert.vue b/components/Alert.vue
new file mode 100644
index 00000000..983a5cfd
--- /dev/null
+++ b/components/Alert.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/components/Header.vue b/components/Header.vue
index 39c96356..ca321cd4 100644
--- a/components/Header.vue
+++ b/components/Header.vue
@@ -39,46 +39,106 @@
diff --git a/components/NounSubmitForm.vue b/components/NounSubmitForm.vue
index 6db6d1d3..8e0323c1 100644
--- a/components/NounSubmitForm.vue
+++ b/components/NounSubmitForm.vue
@@ -32,7 +32,7 @@
nouns.neuter
nouns.neuterShort
-
|
+ |
@@ -118,9 +118,6 @@
import { nounTemplates } from '../src/data';
export default {
- props: {
- secret: {},
- },
data() {
return {
form: {
@@ -142,9 +139,9 @@
methods: {
async submit(event) {
this.submitting = true;
- await this.$axios.$post(`/nouns/submit?secret=${this.secret}`, {
+ await this.$axios.$post(`/nouns/submit`, {
data: this.form,
- });
+ }, { headers: this.$auth() });
this.submitting = false;
this.afterSubmit = true;
diff --git a/components/Share.vue b/components/Share.vue
index 7f71956e..08d266a8 100644
--- a/components/Share.vue
+++ b/components/Share.vue
@@ -59,7 +59,7 @@
data() {
return {
preset: {
- url: process.env.baseUrl + this.$route.path,
+ url: process.env.BASE_URL + this.$route.path,
title: this.title,
extra: {
media: '',
diff --git a/components/T.vue b/components/T.vue
index 9de033c3..d8afb3bd 100644
--- a/components/T.vue
+++ b/components/T.vue
@@ -13,9 +13,12 @@
import t from '../src/translator';
export default {
+ props: {
+ params: {},
+ },
data() {
return {
- txt: t(this.$slots.default[0].text),
+ txt: t(this.$slots.default[0].text, this.params || {}),
}
},
}
diff --git a/locale/en/config.suml b/locale/en/config.suml
index 89b6bcca..526cf8dd 100644
--- a/locale/en/config.suml
+++ b/locale/en/config.suml
@@ -74,4 +74,8 @@ contact:
areas:
social_media: ~
+user:
+ enabled: true
+ route: 'account'
+
redirects: []
diff --git a/locale/pl/config.suml b/locale/pl/config.suml
index f8354c1c..eab1136a 100644
--- a/locale/pl/config.suml
+++ b/locale/pl/config.suml
@@ -203,6 +203,10 @@ support:
url: 'https://paypal.me/AndreAvris'
headline: 'PayPal'
+user:
+ enabled: true
+ route: 'konto'
+
redirects:
- { from: '^/neutratywy', to: '/rzeczowniki' }
- { from: '^/literatura', to: '/korpus' }
diff --git a/locale/pl/translations.suml b/locale/pl/translations.suml
index 841dc7c3..7e7dded6 100644
--- a/locale/pl/translations.suml
+++ b/locale/pl/translations.suml
@@ -578,6 +578,36 @@ support:
Jeśli chcesz się zrzucić na serwer, domeny, wlepki itp., lub zwyczajnie postawić autorzom piwo,
możesz skorzystać z poniższego linku (wspomnij o „zaimki.pl” w opisie transakcji):
+user:
+ header: 'Konto'
+ headerLong: 'Twoje konto'
+ tokenExpired: 'Sesja wygasła. Odśwież stronę i spróbuj ponownie.'
+ login:
+ placeholder: 'Email (lub nazwa użytkownika, jeśli już posiadasz konto)'
+ action: 'Zaloguj'
+ emailSent: 'Na adres %email% wysłałośmy email z sześciocyfrowym kodem. Wpisz go poniżej. Kod jest jednorazowy i ważny przez 15 minut.'
+ userNotFound: 'Użytkownik nie został znaleziony.'
+ email:
+ subject: 'Twój kod logowania to %code%'
+ content: |
+ Aby potwierdzić swój adres email, użyj kodu: %code%.
+
+ Jeśli nie zamawiałxś tego kodu, po prostu zignoruj tę wiadomość.
+ code:
+ action: 'Sprawdź'
+ invalid: 'Kod nieprawidłowy.'
+ account:
+ changeUsername:
+ header: 'Nazwa użytkownika'
+ action: 'Zmień'
+ invalid: 'Nazwa użytkownika musi mieć od 4 do 16 znaków i zawierać wyłącznie cyfry, litery, kropkę, myślnik i podłogę.'
+ taken: 'Ta nazwa użytkownika jest zajęta.'
+ changeEmail:
+ header: 'Adres email'
+ action: 'Zmień'
+ admin: 'Adminię'
+ logout: 'Wyloguj'
+
share: 'Udostępnij'
crud:
diff --git a/migrations/002-users.sql b/migrations/002-users.sql
new file mode 100644
index 00000000..d461d18c
--- /dev/null
+++ b/migrations/002-users.sql
@@ -0,0 +1,23 @@
+-- Up
+
+CREATE TABLE users (
+ id TEXT NOT NULL PRIMARY KEY,
+ username TEXT NOT NULL,
+ email TEXT NOT NULL,
+ roles TEXT NOT NULL,
+ avatarSource TEXT
+);
+
+CREATE TABLE authenticators (
+ id TEXT NOT NULL PRIMARY KEY,
+ userId TEXT,
+ type TEXT NOT NULL,
+ payload TEXT NOT NULL,
+ validUntil INTEGER,
+ FOREIGN KEY(userId) REFERENCES users(id)
+);
+
+-- Down
+
+DROP TABLE authenticators;
+DROP TABLE users;
diff --git a/nuxt.config.js b/nuxt.config.js
index da3aed0a..5c563962 100644
--- a/nuxt.config.js
+++ b/nuxt.config.js
@@ -1,5 +1,6 @@
import translations from './server/translations';
import config from './server/config';
+import fs from 'fs';
const locale = config.locale;
const title = translations.title;
@@ -43,6 +44,7 @@ export default {
plugins: [
{ src: '~/plugins/vue-matomo.js', ssr: false },
{ src: '~/plugins/globals.js' },
+ { src: '~/plugins/auth.js' },
],
components: true,
buildModules: [],
@@ -51,7 +53,8 @@ export default {
'@nuxtjs/axios',
['@nuxtjs/redirect-module', {
rules: config.redirects,
- }]
+ }],
+ 'cookie-universal-nuxt',
],
pwa: {
manifest: {
@@ -82,17 +85,17 @@ export default {
},
},
env: {
- baseUrl: process.env.BASE_URL,
- secret: process.env.SECRET,
- lang: locale,
+ BASE_URL: process.env.BASE_URL,
+ PUBLIC_KEY: fs.readFileSync(__dirname + '/keys/public.pem').toString(),
},
serverMiddleware: {
'/': bodyParser.json(),
- '/nouns': '~/server/nouns.js',
'/banner': '~/server/banner.js',
+ '/api/nouns': '~/server/nouns.js',
+ '/api/user': '~/server/user.js',
},
axios: {
- baseURL: process.env.BASE_URL,
+ baseURL: process.env.BASE_URL + '/api',
},
router: {
extendRoutes(routes, resolve) {
@@ -128,6 +131,9 @@ export default {
routes.push({ path: '/' + config.contact.route, component: resolve(__dirname, 'routes/contact.vue') });
}
+ if (config.user.enabled) {
+ routes.push({path: '/' + config.user.route, component: resolve(__dirname, 'routes/user.vue')});
+ }
routes.push({ path: '/' + config.template.any.route, component: resolve(__dirname, 'routes/any.vue') });
routes.push({ name: 'all', path: '*', component: resolve(__dirname, 'routes/template.vue') });
diff --git a/package.json b/package.json
index dceead6c..07b4d7fc 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,9 @@
"@nuxtjs/redirect-module": "^0.3.1",
"body-parser": "^1.19.0",
"canvas": "^2.6.1",
+ "cookie-universal-nuxt": "^2.1.4",
"dotenv": "^8.2.0",
+ "jsonwebtoken": "^8.5.1",
"mailer": "^0.6.7",
"nuxt": "^2.13.0",
"sql-template-strings": "^2.2.2",
diff --git a/pages/index.vue b/pages/index.vue
index 92b3e51b..a2d3a482 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -242,14 +242,14 @@
if (!this.selectedTemplate.pronoun()) {
return null;
}
- return this.addSlash(process.env.baseUrl + '/' + (this.usedBaseEquals ? this.usedBase : this.longLink));
+ return this.addSlash(process.env.BASE_URL + '/' + (this.usedBaseEquals ? this.usedBase : this.longLink));
},
linkMultiple() {
if (!this.multiple.length) {
return null;
}
- return this.addSlash(process.env.baseUrl + '/' + this.multiple.join('&'));
+ return this.addSlash(process.env.BASE_URL + '/' + this.multiple.join('&'));
},
sources() {
return getSources(this.selectedTemplate);
diff --git a/plugins/auth.js b/plugins/auth.js
new file mode 100644
index 00000000..53b6ee79
--- /dev/null
+++ b/plugins/auth.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import t from "../src/translator";
+
+export default ({app, store}) => {
+ const token = app.$cookies.get('token');
+ if (token) {
+ store.commit('setToken', token);
+ if (!store.state.token) {
+ app.$cookies.removeAll();
+ }
+ }
+
+ Vue.prototype.$user = _ => store.state.user;
+ Vue.prototype.$auth = _ => {
+ return store.state.token ? {
+ authorization: 'Bearer ' + store.state.token,
+ } : {};
+ };
+ Vue.prototype.$admin = _ => {
+ return store.state.user && store.state.user.authenticated && store.state.user.roles === 'admin';
+ };
+}
diff --git a/routes/contact.vue b/routes/contact.vue
index ddfd98c2..974ba99a 100644
--- a/routes/contact.vue
+++ b/routes/contact.vue
@@ -42,10 +42,8 @@
diff --git a/server/authenticate.js b/server/authenticate.js
new file mode 100644
index 00000000..6345105c
--- /dev/null
+++ b/server/authenticate.js
@@ -0,0 +1,9 @@
+import jwt from './jwt';
+
+export default ({headers: { authorization }}) => {
+ if (!authorization || !authorization.startsWith('Bearer ')) {
+ return null;
+ }
+
+ return jwt.validate(authorization.substring(7));
+}
diff --git a/server/jwt.js b/server/jwt.js
new file mode 100644
index 00000000..b3b625b4
--- /dev/null
+++ b/server/jwt.js
@@ -0,0 +1,32 @@
+import jwt from 'jsonwebtoken';
+import fs from 'fs';
+
+class Jwt {
+ constructor(privateKey, publicKey) {
+ this.privateKey = fs.readFileSync(privateKey);
+ this.publicKey = fs.readFileSync(publicKey);
+ }
+
+ sign(payload, expiresIn = '30d') {
+ return jwt.sign(payload, this.privateKey, {
+ expiresIn,
+ algorithm: 'RS256',
+ audience: process.env.BASE_URL,
+ issuer: process.env.BASE_URL,
+ });
+ }
+
+ validate(token) {
+ try {
+ return jwt.verify(token, this.publicKey, {
+ algorithm: 'RS256',
+ audience: process.env.BASE_URL,
+ issuer: process.env.BASE_URL,
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ }
+}
+
+export default new Jwt(__dirname + '/../keys/private.pem', __dirname + '/../keys/public.pem');
diff --git a/server/mailer.js b/server/mailer.js
new file mode 100644
index 00000000..703350d0
--- /dev/null
+++ b/server/mailer.js
@@ -0,0 +1,21 @@
+const mailer = require('mailer');
+
+module.exports = (to, subject, body) => {
+ mailer.send({
+ host: process.env.MAILER_HOST,
+ port: parseInt(process.env.MAILER_PORT),
+ ssl: parseInt(process.env.MAILER_PORT) === 465,
+ authentication: 'login',
+ username: process.env.MAILER_USER,
+ password: process.env.MAILER_PASS,
+ from: process.env.MAILER_FROM,
+ to,
+ subject,
+ body,
+ },
+ function(err){
+ if (err) {
+ console.error(err);
+ }
+ });
+}
diff --git a/server/notify.js b/server/notify.js
index 2a6faf6d..eac7f0f8 100644
--- a/server/notify.js
+++ b/server/notify.js
@@ -1,6 +1,6 @@
const dbConnection = require('./db');
-const mailer = require('mailer');
require('dotenv').config({ path:__dirname + '/../.env' });
+const mailer = require('./mailer');
async function notify() {
const db = await dbConnection();
@@ -15,23 +15,7 @@ async function notify() {
for (let admin of process.env.MAILER_ADMINS.split(',')) {
console.log('Sending email to ' + admin)
- mailer.send({
- host: process.env.MAILER_HOST,
- port: parseInt(process.env.MAILER_PORT),
- ssl: parseInt(process.env.MAILER_PORT) === 465,
- authentication: 'login',
- username: process.env.MAILER_USER,
- password: process.env.MAILER_PASS,
- from: process.env.MAILER_FROM,
- to: admin,
- subject: '[Zaimki.pl] Wpisy oczekują na moderację',
- body: 'Liczba wpisów: ' + awaitingModeration,
- },
- function(err, result){
- if (err) {
- console.log(err);
- }
- });
+ mailer(admin, '[Zaimki.pl] Wpisy oczekują na moderację', 'Liczba wpisów: ' + awaitingModeration);
}
}
diff --git a/server/nouns.js b/server/nouns.js
index c309f4f0..6a1d4e31 100644
--- a/server/nouns.js
+++ b/server/nouns.js
@@ -1,6 +1,7 @@
const dbConnection = require('./db');
const SQL = require('sql-template-strings');
import { ulid } from 'ulid'
+import authenticate from './authenticate';
const parseQuery = (queryString) => {
const query = {};
@@ -64,20 +65,17 @@ const isTroll = (body) => {
export default async function (req, res, next) {
const db = await dbConnection();
-
- const [url, queryString] = req.url.split('?');
- const query = parseQuery(queryString || '');
-
- const isAdmin = query['secret'] === process.env.SECRET;
+ const user = authenticate(req);
+ const isAdmin = user && user.authenticated && user.roles === 'admin';
let result = {error: 'Not found'}
- if (req.method === 'GET' && url === '/all') {
+ if (req.method === 'GET' && req.url === '/all') {
result = await db.all(`
SELECT * FROM nouns
${isAdmin ? '' : 'WHERE approved = 1'}
ORDER BY approved, masc
`);
- } else if (req.method === 'POST' && url === '/submit') {
+ } else if (req.method === 'POST' && req.url === '/submit') {
if (isAdmin || !isTroll(req.body.data)) {
const id = ulid()
await db.get(SQL`
@@ -94,14 +92,14 @@ export default async function (req, res, next) {
}
}
result = 'ok';
- } else if (req.method === 'POST' && url.startsWith('/approve/') && isAdmin) {
- await approve(db, getId(url));
+ } else if (req.method === 'POST' && req.url.startsWith('/approve/') && isAdmin) {
+ await approve(db, getId(req.url));
result = 'ok';
- } else if (req.method === 'POST' && url.startsWith('/hide/') && isAdmin) {
- await hide(db, getId(url));
+ } else if (req.method === 'POST' && req.url.startsWith('/hide/') && isAdmin) {
+ await hide(db, getId(req.url));
result = 'ok';
- } else if (req.method === 'POST' && url.startsWith('/remove/') && isAdmin) {
- await remove(db, getId(url));
+ } else if (req.method === 'POST' && req.url.startsWith('/remove/') && isAdmin) {
+ await remove(db, getId(req.url));
result = 'ok';
}
diff --git a/server/user.js b/server/user.js
new file mode 100644
index 00000000..0b089640
--- /dev/null
+++ b/server/user.js
@@ -0,0 +1,177 @@
+import jwt from './jwt';
+import { makeId } 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 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 = ${usernameOrEmail}`);
+ } else {
+ user = await db.get(SQL`SELECT * FROM users WHERE username = ${usernameOrEmail}`);
+ }
+
+ if (!user && !isEmail) {
+ return {error: 'user.login.userNotFound'}
+ }
+
+ const payload = {
+ username: isEmail ? (user ? user.username : null) : usernameOrEmail,
+ email: isEmail ? 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 !== 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 username = ${proposal}`);
+ if (!dbUser) {
+ return proposal;
+ }
+ c++;
+ }
+}
+
+const issueAuthentication = async (db, user) => {
+ let dbUser = await db.get(SQL`SELECT * FROM users WHERE email = ${user.email}`);
+ if (!dbUser) {
+ dbUser = {
+ id: ulid(),
+ username: await defaultUsername(db, user.email),
+ email: user.email,
+ roles: 'user',
+ avatarSource: null,
+ }
+ }
+
+ 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 username = ${username}`);
+ if (dbUser) {
+ return { error: 'user.account.changeUsername.taken' }
+ }
+
+ await db.get(SQL`UPDATE users SET username = ${username} WHERE email = ${user.email}`);
+
+ return await issueAuthentication(db, user);
+}
+
+export default async function (req, res, next) {
+ const db = await dbConnection();
+ const user = authenticate(req);
+
+ let result = {error: 'notfound'}
+
+ if (req.method === 'POST' && req.url === '/init' && req.body.usernameOrEmail) {
+ result = await init(db, req.body.usernameOrEmail)
+ } else if (req.method === 'POST' && req.url === '/validate' && req.body.code) {
+ result = await validate(db, user, req.body.code);
+ } else if (req.method === 'POST' && req.url === '/change-username' && user && user.authenticated && req.body.username) {
+ result = await changeUsername(db, user, req.body.username);
+ }
+
+ res.setHeader('content-type', 'application/json');
+ res.write(JSON.stringify(result));
+ res.end();
+}
diff --git a/src/helpers.js b/src/helpers.js
index 5814537a..f76d49d6 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -31,7 +31,7 @@ export const head = ({title, description, banner}) => {
}
if (banner) {
- banner = process.env.baseUrl + '/' + banner;
+ banner = process.env.BASE_URL + '/' + banner;
meta.meta.push({ hid: 'og:logo', property: 'og:logo', content: banner });
meta.meta.push({ hid: 'twitter:image', property: 'twitter:image', content: banner });
}
@@ -62,3 +62,13 @@ export const clearUrl = url => {
return decodeURIComponent(url);
}
+
+export const makeId = (length, characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') => {
+ let result = '';
+ const charactersLength = characters.length;
+ for (let i = 0; i < length; i++) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ }
+
+ return result;
+}
diff --git a/src/translator.js b/src/translator.js
index 60224b13..bb76c13f 100644
--- a/src/translator.js
+++ b/src/translator.js
@@ -1,6 +1,6 @@
import translations from '../data/translations.suml';
-export default key => {
+export default (key, params = {}) => {
let value = translations;
for (let part of key.split('.')) {
value = value[part];
@@ -10,5 +10,11 @@ export default key => {
}
}
+ for (let k in params) {
+ if (params.hasOwnProperty(k)) {
+ value = value.replace(new RegExp('%' + k + '%', 'g'), params[k])
+ }
+ }
+
return value;
}
diff --git a/store/index.js b/store/index.js
new file mode 100644
index 00000000..3ba66f36
--- /dev/null
+++ b/store/index.js
@@ -0,0 +1,36 @@
+import jwt from 'jsonwebtoken';
+
+export const state = () => ({
+ token: null,
+ user: null,
+})
+
+export const mutations = {
+ setToken(state, token) {
+ if (!token) {
+ state.token = null;
+ state.user = null;
+ return;
+ }
+
+ let user;
+ try {
+ user = jwt.verify(token, process.env.PUBLIC_KEY, {
+ algorithm: 'RS256',
+ audience: process.env.BASE_URL,
+ issuer: process.env.BASE_URL,
+ });
+ } catch {
+ user = null;
+ }
+
+ if (user && user.authenticated) {
+ state.token = token;
+ state.user = user;
+ return;
+ }
+
+ state.token = null;
+ state.user = null;
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 9a1f75ab..ccbe2b16 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1198,6 +1198,11 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+"@types/cookie@^0.3.3":
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
+ integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==
+
"@types/html-minifier-terser@^5.0.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880"
@@ -2070,6 +2075,11 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.6.4, browserslist@^4.
escalade "^3.0.1"
node-releases "^1.1.58"
+buffer-equal-constant-time@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+ integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
+
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -2617,6 +2627,22 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
+cookie-universal-nuxt@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/cookie-universal-nuxt/-/cookie-universal-nuxt-2.1.4.tgz#323f8645501f88cb2422127ad8ba2ee40187b716"
+ integrity sha512-xbn4Ozs9S0u2+0mQTZRwGlBL9MGNq8N4H6iGfprR5ufZFCS2hGef++3DBHSmHXZi30Wu3Q7RI/GkNMhz3cecmg==
+ dependencies:
+ "@types/cookie" "^0.3.3"
+ cookie-universal "^2.1.4"
+
+cookie-universal@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/cookie-universal/-/cookie-universal-2.1.4.tgz#826a273da7eb9b08bfb0139bae12ea70770d564b"
+ integrity sha512-dwWXs7NGBzaBYDypu3jWH5M3NJW+zu5QdyJkFMHJvhLuyL4/eXG4105fwtTDwfIqyTunwVvQX4PHdtfPDS7URQ==
+ dependencies:
+ "@types/cookie" "^0.3.3"
+ cookie "^0.4.0"
+
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
@@ -2627,6 +2653,11 @@ cookie@^0.3.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
+cookie@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
+ integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
+
copy-concurrently@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
@@ -3231,6 +3262,13 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
+ecdsa-sig-formatter@1.0.11:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
+ integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
+ dependencies:
+ safe-buffer "^5.0.1"
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -4876,6 +4914,22 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
+jsonwebtoken@^8.5.1:
+ version "8.5.1"
+ resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
+ integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
+ dependencies:
+ jws "^3.2.2"
+ lodash.includes "^4.3.0"
+ lodash.isboolean "^3.0.3"
+ lodash.isinteger "^4.0.4"
+ lodash.isnumber "^3.0.3"
+ lodash.isplainobject "^4.0.6"
+ lodash.isstring "^4.0.1"
+ lodash.once "^4.0.0"
+ ms "^2.1.1"
+ semver "^5.6.0"
+
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -4886,6 +4940,23 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
+jwa@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
+ integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
+ dependencies:
+ buffer-equal-constant-time "1.0.1"
+ ecdsa-sig-formatter "1.0.11"
+ safe-buffer "^5.0.1"
+
+jws@^3.2.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
+ integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
+ dependencies:
+ jwa "^1.4.1"
+ safe-buffer "^5.0.1"
+
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -5005,6 +5076,36 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
+lodash.includes@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
+ integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
+
+lodash.isboolean@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
+ integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
+
+lodash.isinteger@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
+ integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
+
+lodash.isnumber@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
+ integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
+
+lodash.isplainobject@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+ integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+
+lodash.isstring@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
+ integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
+
lodash.kebabcase@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
@@ -5015,6 +5116,11 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+lodash.once@^4.0.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
+ integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
+
lodash.template@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"