Merge branch 'accounts' into main
This commit is contained in:
commit
bd771b4c3c
|
@ -1,5 +1,4 @@
|
|||
BASE_URL=http://localhost:3000
|
||||
SECRET=secret
|
||||
|
||||
MAILER_HOST=
|
||||
MAILER_PORT=
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
/daemonise.json
|
||||
/daemonise.log
|
||||
|
||||
/keys
|
||||
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
|
|
7
Makefile
7
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
|
||||
|
||||
|
|
15
README.md
15
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)
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<section>
|
||||
<Alert type="danger" :message="error"/>
|
||||
|
||||
<form @submit.prevent="changeUsername">
|
||||
<h3 class="h6"><T>user.account.changeUsername.header</T></h3>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" v-model="username"
|
||||
required minlength="4" maxlength="16"/>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-primary">
|
||||
<T>user.account.changeUsername.action</T>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<h3 class="h6"><T>user.account.changeEmail.header</T></h3>
|
||||
<p>{{ email }}</p>
|
||||
</div>
|
||||
|
||||
<p v-if="$admin()">
|
||||
<span class="badge badge-primary"><T>user.account.admin</T></span>
|
||||
</p>
|
||||
|
||||
<button class="btn btn-outline-secondary btn-sm" @click="logout">
|
||||
<Icon v="sign-out"/>
|
||||
<T>user.logout</T>
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: this.$user().username,
|
||||
email: this.$user().email,
|
||||
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async changeUsername() {
|
||||
await this.post(`/user/change-username`, {
|
||||
username: this.username
|
||||
}, { headers: this.$auth() });
|
||||
},
|
||||
async post(url, data, options = {}) {
|
||||
this.error = '';
|
||||
|
||||
const response = await this.$axios.$post(url, data, options);
|
||||
|
||||
if (response.error) {
|
||||
this.error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit('setToken', response.token);
|
||||
this.$cookies.set('token', this.$store.state.token);
|
||||
},
|
||||
logout() {
|
||||
this.$store.commit('setToken', null);
|
||||
this.$cookies.removeAll();
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<div v-if="message" :class="'alert alert-' + type">
|
||||
<p class="mb-0">
|
||||
<Icon v="exclamation-triangle"/>
|
||||
<T>{{message}}</T>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
type: { required: true },
|
||||
message: {},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -39,46 +39,106 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
const links = [];
|
||||
links.push({ link: '/', icon: 'home', text: this.$t('home.header'), textLong: this.$t('home.headerLong'), extra: ['all', this.config.template.any.route] });
|
||||
computed: {
|
||||
...mapState([
|
||||
'user',
|
||||
]),
|
||||
links() {
|
||||
const links = [];
|
||||
|
||||
if (this.config.sources.enabled) {
|
||||
links.push({ link: '/' + this.config.sources.route, icon: 'books', text: this.$t('sources.header'), textLong: this.$t('sources.headerLong') });
|
||||
}
|
||||
links.push({
|
||||
link: '/',
|
||||
icon: 'home',
|
||||
text: this.$t('home.header'),
|
||||
textLong: this.$t('home.headerLong'),
|
||||
extra: ['all', this.config.template.any.route],
|
||||
});
|
||||
|
||||
if (this.config.nouns.enabled) {
|
||||
links.push({ link: '/' + this.config.nouns.route, icon: 'atom-alt', text: this.$t('nouns.header'), textLong: this.$t('nouns.headerLong') });
|
||||
}
|
||||
if (this.config.sources.enabled) {
|
||||
links.push({
|
||||
link: '/' + this.config.sources.route,
|
||||
icon: 'books',
|
||||
text: this.$t('sources.header'),
|
||||
textLong: this.$t('sources.headerLong'),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.names.enabled) {
|
||||
links.push({ link: '/' + this.config.names.route, icon: 'signature', text: this.$t('names.header'), textLong: this.$t('names.headerLong') });
|
||||
}
|
||||
if (this.config.nouns.enabled) {
|
||||
links.push({
|
||||
link: '/' + this.config.nouns.route,
|
||||
icon: 'atom-alt',
|
||||
text: this.$t('nouns.header'),
|
||||
textLong: this.$t('nouns.headerLong'),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.faq.enabled) {
|
||||
links.push({ link: '/' + this.config.faq.route, icon: 'map-marker-question', text: this.$t('faq.header'), textLong: this.$t('faq.headerLong') });
|
||||
}
|
||||
if (this.config.names.enabled) {
|
||||
links.push({
|
||||
link: '/' + this.config.names.route,
|
||||
icon: 'signature',
|
||||
text: this.$t('names.header'),
|
||||
textLong: this.$t('names.headerLong'),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.links.enabled) {
|
||||
links.push({ link: '/' + this.config.links.route, icon: 'bookmark', text: this.$t('links.header'), textLong: this.$t('links.headerLong') });
|
||||
}
|
||||
if (this.config.faq.enabled) {
|
||||
links.push({
|
||||
link: '/' + this.config.faq.route,
|
||||
icon: 'map-marker-question',
|
||||
text: this.$t('faq.header'),
|
||||
textLong: this.$t('faq.headerLong'),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.people.enabled) {
|
||||
links.push({ link: '/' + this.config.people.route, icon: 'user-friends', text: this.$t('people.header'), textLong: this.$t('people.headerLong') });
|
||||
}
|
||||
if (this.config.links.enabled) {
|
||||
links.push({
|
||||
link: '/' + this.config.links.route,
|
||||
icon: 'bookmark',
|
||||
text: this.$t('links.header'),
|
||||
textLong: this.$t('links.headerLong'),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.english.enabled) {
|
||||
links.push({ link: '/' + this.config.english.route, icon: 'globe-americas', text: this.$t('english.header'), textLong: this.$t('english.headerLong') });
|
||||
}
|
||||
if (this.config.people.enabled) {
|
||||
links.push({
|
||||
link: '/' + this.config.people.route,
|
||||
icon: 'user-friends',
|
||||
text: this.$t('people.header'),
|
||||
textLong: this.$t('people.headerLong'),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.contact.enabled) {
|
||||
links.push({ link: '/' + this.config.contact.route, icon: 'comment-alt-smile', text: this.$t('contact.header')});
|
||||
}
|
||||
if (this.config.english.enabled) {
|
||||
links.push({
|
||||
link: '/' + this.config.english.route,
|
||||
icon: 'globe-americas',
|
||||
text: this.$t('english.header'),
|
||||
textLong: this.$t('english.headerLong'),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
links,
|
||||
};
|
||||
if (this.config.contact.enabled) {
|
||||
links.push({
|
||||
link: '/' + this.config.contact.route,
|
||||
icon: 'comment-alt-smile',
|
||||
text: this.$t('contact.header'),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.user.enabled) {
|
||||
links.push({
|
||||
link: '/' + this.config.user.route,
|
||||
icon: 'user',
|
||||
text: this.user ? '@' + this.user.username : this.$t('user.header'),
|
||||
textLong: this.user ? '@' + this.user.username : this.$t('user.headerLong'),
|
||||
});
|
||||
}
|
||||
|
||||
return links;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isActiveRoute(link) {
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<section>
|
||||
<Alert type="danger" :message="error"/>
|
||||
|
||||
<div v-if="token === null">
|
||||
<form @submit.prevent="login">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" v-model="usernameOrEmail"
|
||||
:placeholder="$t('user.login.placeholder')" autofocus required/>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary">
|
||||
<Icon v="sign-in"/>
|
||||
<T>user.login.action</T>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else-if="payload && !payload.code">
|
||||
<div class="alert alert-success">
|
||||
<p class="mb-0">
|
||||
<Icon v="envelope-open-text"/>
|
||||
<T :params="{email: payload.email}">user.login.emailSent</T>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="validate">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control text-center" v-model="code"
|
||||
placeholder="000000" autofocus required minlength="0" maxlength="6"
|
||||
inputmode="numeric" pattern="[0-9]{6}" autocomplete="one-time-code"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary">
|
||||
<Icon v="key"/>
|
||||
<T>user.code.action</T>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
token: null,
|
||||
usernameOrEmail: '',
|
||||
code: '',
|
||||
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
payload() {
|
||||
if (!this.token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.$store.commit('setToken', this.token);
|
||||
this.$cookies.set('token', this.$store.state.token);
|
||||
|
||||
return jwt.verify(this.token, process.env.PUBLIC_KEY, {
|
||||
algorithm: 'RS256',
|
||||
audience: process.env.BASE_URL,
|
||||
issuer: process.env.BASE_URL,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
await this.post(`/user/init`, {
|
||||
usernameOrEmail: this.usernameOrEmail
|
||||
});
|
||||
},
|
||||
async validate() {
|
||||
await this.post(`/user/validate`, {
|
||||
code: this.code
|
||||
}, {
|
||||
headers: {
|
||||
authorization: 'Bearer ' + this.token,
|
||||
},
|
||||
});
|
||||
},
|
||||
async post(url, data, options = {}) {
|
||||
this.error = '';
|
||||
|
||||
const response = await this.$axios.$post(url, data, options);
|
||||
|
||||
this.usernameOrEmail = '';
|
||||
this.code = '';
|
||||
|
||||
if (response.error) {
|
||||
this.error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
this.token = response.token;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -32,7 +32,7 @@
|
|||
<span class="d-none d-md-inline"><T>nouns.neuter</T></span>
|
||||
<span class="d-md-none"><T>nouns.neuterShort</T></span>
|
||||
</th>
|
||||
<th v-if="secret"></th>
|
||||
<th v-if="$admin()"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -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;
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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 || {}),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -74,4 +74,8 @@ contact:
|
|||
areas:
|
||||
social_media: ~
|
||||
|
||||
user:
|
||||
enabled: true
|
||||
route: 'account'
|
||||
|
||||
redirects: []
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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 <strong>%email%</strong> 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:
|
||||
|
|
|
@ -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;
|
|
@ -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') });
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
};
|
||||
}
|
|
@ -42,10 +42,8 @@
|
|||
|
||||
<script>
|
||||
import { head } from "../src/helpers";
|
||||
import Icon from "../components/Icon";
|
||||
|
||||
export default {
|
||||
components: {Icon},
|
||||
head() {
|
||||
return head({
|
||||
title: this.$t('contact.header'),
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<NounsExtra/>
|
||||
|
||||
<Loading :value="nounsRaw">
|
||||
<section v-if="secret">
|
||||
<section v-if="$admin()">
|
||||
<div class="alert alert-info">
|
||||
<strong>{{ nounsCountApproved() }}</strong> <T>nouns.approved</T>,
|
||||
<strong>{{ nounsCountPending() }}</strong> <T>nouns.pending</T>.
|
||||
|
@ -44,7 +44,7 @@
|
|||
</section>
|
||||
|
||||
<section class="table-responsive">
|
||||
<table :class="'table table-striped table-hover table-fixed-' + (secret ? 4 : 3)">
|
||||
<table :class="'table table-striped table-hover table-fixed-' + ($admin() ? 4 : 3)">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-nowrap">
|
||||
|
@ -59,7 +59,7 @@
|
|||
<Icon v="neuter"/>
|
||||
<T>nouns.neuter</T>
|
||||
</th>
|
||||
<th v-if="secret"></th>
|
||||
<th v-if="$admin()"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -83,7 +83,7 @@
|
|||
</ul>
|
||||
</small>
|
||||
|
||||
<button v-if="!secret" class="btn btn-outline-primary btn-sm m-1 hover-show" @click="edit(noun)">
|
||||
<button v-if="!$admin()" class="btn btn-outline-primary btn-sm m-1 hover-show" @click="edit(noun)">
|
||||
<Icon v="pen"/>
|
||||
<T>nouns.edit</T>
|
||||
</button>
|
||||
|
@ -128,7 +128,7 @@
|
|||
</ul>
|
||||
</small>
|
||||
</td>
|
||||
<td v-if="secret">
|
||||
<td v-if="$admin()">
|
||||
<ul class="list-unstyled">
|
||||
<li v-if="!noun.approved">
|
||||
<button class="btn btn-success btn-sm m-1" @click="approve(noun)">
|
||||
|
@ -160,7 +160,7 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<tr>
|
||||
<td :colspan="secret ? 4 : 3" class="text-center">
|
||||
<td :colspan="$admin() ? 4 : 3" class="text-center">
|
||||
<Icon v="search"/>
|
||||
<T>nouns.empty</T>
|
||||
</td>
|
||||
|
@ -173,7 +173,7 @@
|
|||
|
||||
<Separator icon="plus"/>
|
||||
|
||||
<NounSubmitForm ref="form" :secret="secret"/>
|
||||
<NounSubmitForm ref="form"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -189,12 +189,11 @@
|
|||
return {
|
||||
filter: '',
|
||||
nounsRaw: undefined,
|
||||
secret: this.$route.query.secret,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (process.client) {
|
||||
this.$axios.$get(`/nouns/all?secret=${this.$route.query.secret || ''}`).then(data => {
|
||||
this.$axios.$get(`/nouns/all`, { headers: this.$auth() }).then(data => {
|
||||
this.nounsRaw = data;
|
||||
});
|
||||
if (window.location.hash) {
|
||||
|
@ -221,7 +220,7 @@
|
|||
this.$refs.form.edit(noun);
|
||||
},
|
||||
async approve(noun) {
|
||||
await this.$axios.$post(`/nouns/approve/${noun.id}?secret=${this.secret || ''}`);
|
||||
await this.$axios.$post(`/nouns/approve/${noun.id}`, {}, { headers: this.$auth() });
|
||||
if (noun.base) {
|
||||
delete this.nouns[noun.base];
|
||||
}
|
||||
|
@ -230,7 +229,7 @@
|
|||
this.$forceUpdate();
|
||||
},
|
||||
async hide(noun) {
|
||||
await this.$axios.$post(`/nouns/hide/${noun.id}?secret=${this.secret || ''}`);
|
||||
await this.$axios.$post(`/nouns/hide/${noun.id}`, {}, { headers: this.$auth() });
|
||||
noun.approved = false;
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
@ -238,7 +237,7 @@
|
|||
if (!confirm('Czy na pewno usunąć ten wpis?')) {
|
||||
return false;
|
||||
}
|
||||
await this.$axios.$post(`/nouns/remove/${noun.id}?secret=${this.secret || ''}`);
|
||||
await this.$axios.$post(`/nouns/remove/${noun.id}`, {}, { headers: this.$auth() });
|
||||
delete this.nouns[noun.id];
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<h2>
|
||||
<Icon v="user"/>
|
||||
<T>user.headerLong</T>
|
||||
</h2>
|
||||
|
||||
<Account v-if="$user()"/>
|
||||
<Login v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { head } from "../src/helpers";
|
||||
|
||||
export default {
|
||||
head() {
|
||||
return head({
|
||||
title: this.$t('user.headerLong'),
|
||||
});
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -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));
|
||||
}
|
|
@ -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');
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
106
yarn.lock
106
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"
|
||||
|
|
Reference in New Issue