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 @@ + + + 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"