#74 social login

This commit is contained in:
Avris 2020-11-02 19:31:05 +01:00
parent 9d94cf84e0
commit fa9bdee3ee
20 changed files with 426 additions and 48 deletions

View File

@ -1,8 +1,19 @@
BASE_URL=http://localhost:3000
SECRET=
MAILER_HOST=
MAILER_PORT=
MAILER_USER=
MAILER_PASS=
MAILER_FROM=
MAILER_ADMINS=
TWITTER_KEY=
TWITTER_SECRET=
FACEBOOK_KEY=
FACEBOOK_SECRET=
GOOGLE_KEY=
GOOGLE_SECRET=

View File

@ -67,14 +67,27 @@
</div>
</div>
<Loading :value="profiles"><template v-if="profiles !== undefined">
<h3 class="h4"><T>profile.list</T>:</h3>
<ul class="list-group mb-3">
<Loading :value="socialConnections">
<template v-slot:header>
<h3 class="h4"><T>user.socialConnection.list</T>:</h3>
</template>
<ul v-if="socialConnections !== undefined" class="list-group">
<li v-for="(providerOptions, provider) in socialProviders" :key="provider" :class="['list-group-item', 'en' === config.locale ? 'profile-current' : '']">
<SocialConnection :provider="provider" :providerOptions="providerOptions" :connection="socialConnections[provider]" @disconnected="socialConnections[provider] = undefined"/>
</li>
</ul>
</Loading>
<Loading :value="profiles">
<template v-slot:header>
<h3 class="h4"><T>profile.list</T>:</h3>
</template>
<ul v-if="profiles !== undefined" class="list-group">
<li v-for="(options, locale) in locales" :key="locale" :class="['list-group-item', locale === config.locale ? 'profile-current' : '']">
<ProfileOverview :profile="profiles[locale]" :locale="locale" @update="setProfiles"/>
</li>
</ul>
</template></Loading>
</Loading>
<section>
<a href="#" class="badge badge-light border" @click.prevent="logout">
@ -91,6 +104,8 @@
</template>
<script>
import {socialProviders} from "../src/data";
export default {
data() {
return {
@ -102,10 +117,14 @@
code: '',
profiles: undefined,
socialProviders,
socialConnections: undefined,
}
},
async mounted() {
this.profiles = await this.$axios.$get(`/profile/get/${this.$user().username}`);
this.socialConnections = await this.$axios.$get(`/user/social-connections`);
},
methods: {
async changeUsername() {
@ -113,7 +132,7 @@
const response = await this.$axios.$post(`/user/change-username`, {
username: this.username,
}, { headers: this.$auth() });
});
if (response.error) {
this.error = response.error;
@ -130,7 +149,7 @@
email: this.email,
authId: this.changeEmailAuthId,
code: this.code,
}, { headers: this.$auth() });
});
if (response.error) {
this.error = response.error;
@ -160,7 +179,7 @@
async deleteAccount() {
await this.$confirm(this.$t('user.deleteAccountConfirm'), 'danger');
const response = await this.$axios.$post(`/user/delete`, {}, { headers: this.$auth() });
const response = await this.$axios.$post(`/user/delete`);
this.logout();
},

View File

@ -1,5 +1,5 @@
<template>
<img :src="gravatar" alt="" class="rounded-circle" :style="`width: 100%;max-width: ${dsize};max-height: ${dsize};`"/>
<img :src="src || gravatar" alt="" class="rounded-circle" :style="`width: 100%;max-width: ${dsize};max-height: ${dsize};`"/>
</template>
<script>
@ -10,6 +10,7 @@
user: { required: true },
size: { 'default': 128 },
dsize: { 'default': '6rem' },
src: {},
},
data() {
return {

View File

@ -182,7 +182,7 @@
if (this.nounsRaw !== undefined) {
return;
}
this.nounsRaw = await this.$axios.$get(`/nouns/all/${this.config.locale}`, { headers: this.$auth() });
this.nounsRaw = await this.$axios.$get(`/nouns/all/${this.config.locale}`);
},
async setFilter(filter) {
this.filter = filter;
@ -200,7 +200,7 @@
this.$refs.form.edit(noun);
},
async approve(noun) {
await this.$axios.$post(`/nouns/approve/${noun.id}`, {}, { headers: this.$auth() });
await this.$axios.$post(`/nouns/approve/${noun.id}`);
if (noun.base) {
delete this.nouns[noun.base];
}
@ -209,7 +209,7 @@
this.$forceUpdate();
},
async hide(noun) {
await this.$axios.$post(`/nouns/hide/${noun.id}`, {}, { headers: this.$auth() });
await this.$axios.$post(`/nouns/hide/${noun.id}`);
noun.approved = false;
this.$forceUpdate();
},
@ -217,7 +217,7 @@
if (!confirm('Czy na pewno usunąć ten wpis?')) {
return false;
}
await this.$axios.$post(`/nouns/remove/${noun.id}`, {}, { headers: this.$auth() });
await this.$axios.$post(`/nouns/remove/${noun.id}`);
delete this.nouns[noun.id];
this.$forceUpdate();
},

View File

@ -1,5 +1,6 @@
<template>
<section class="w-100">
<slot name="header"></slot>
<div v-if="isLoaded">
<slot/>
</div>

View File

@ -4,6 +4,10 @@
<div v-if="token === null">
<form @submit.prevent="login">
<p>
<Icon v="info-circle"/>
<T>user.login.why</T>
</p>
<div class="input-group mb-3">
<input type="text" class="form-control" v-model="usernameOrEmail"
:placeholder="$t('user.login.placeholder')" autofocus required/>
@ -14,9 +18,12 @@
</button>
</div>
</div>
<p class="small">
<T>user.login.why</T>
</p>
<div class="btn-group btn-block mb-3">
<a :href="`/api/connect/${provider}`" v-for="(providerOptions, provider) in socialProviders" class="btn btn-outline-primary">
<Icon :v="providerOptions.icon || provider" set="b"/>
{{ providerOptions.name }}
</a>
</div>
<p class="small text-muted">
<T>terms.consent</T>
<nuxt-link :to="`/${config.user.termsRoute}`">
@ -54,6 +61,7 @@
<script>
import jwt from 'jsonwebtoken';
import {socialProviders} from "../src/data";
export default {
data() {
@ -63,6 +71,8 @@
code: '',
error: '',
socialProviders,
};
},
computed: {

View File

@ -142,7 +142,7 @@
methods: {
async submit(event) {
this.submitting = true;
await this.$axios.$post(`/nouns/submit/${this.config.locale}`, this.form, { headers: this.$auth() });
await this.$axios.$post(`/nouns/submit/${this.config.locale}`, this.form);
this.submitting = false;
this.afterSubmit = true;

View File

@ -40,7 +40,7 @@
await this.$confirm(this.$t('profile.deleteConfirm'), 'danger');
this.deleting = true;
const response = await this.$axios.$post(`/profile/delete/${locale}`, {}, { headers: this.$auth() });
const response = await this.$axios.$post(`/profile/delete/${locale}`);
this.deleting = false;
this.$emit('update', response);
},

View File

@ -0,0 +1,55 @@
<template>
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center">
<span class="my-2">
<Icon :v="providerOptions.icon || provider" set="b"/>
{{ providerOptions.name }}
</span>
<span v-if="connection === undefined">
<a :href="`/api/connect/${provider}`" class="badge badge-light border">
<Icon v="link"/>
<T>user.socialConnection.connect</T>
</a>
</span>
<span v-else class="text-center">
<span class="mr-3">
<Avatar :src="connection.avatar" :user="$user()" dsize="2rem"/>
{{connection.name}}
</span>
<br class="d-md-none"/>
<a :href="`/api/connect/${provider}`" class="badge badge-light border">
<Icon v="sync"/>
<T>user.socialConnection.refresh</T>
</a>
<Spinner v-if="disconnecting"/>
<a v-else href="#" class="badge badge-light" @click.prevent="disconnect">
<Icon v="unlink"/>
<T>user.socialConnection.disconnect</T>
</a>
</span>
</div>
</template>
<script>
export default {
props: {
provider: { required: true },
providerOptions: { required: true },
connection: {},
},
data() {
return {
disconnecting: false,
}
},
methods: {
async disconnect() {
await this.$confirm(this.$t('user.socialConnection.disconnectConfirm', {email: this.$user().email}), 'danger');
this.disconnecting = true;
const response = await this.$axios.$post(`/user/social-connection/${this.provider}/disconnect`);
this.disconnecting = false;
this.$emit('disconnected', response);
},
},
}
</script>

View File

@ -100,7 +100,7 @@
methods: {
async submit() {
this.submitting = true;
await this.$axios.$post(`/sources/submit/${this.config.locale}`, this.form, { headers: this.$auth() });
await this.$axios.$post(`/sources/submit/${this.config.locale}`, this.form);
this.submitting = false;
this.afterSubmit = true;

View File

@ -684,7 +684,7 @@ user:
Jeśli nie zamawiałxś tego kodu, po prostu zignoruj tę wiadomość.
why: >
Założenie konta pozwala na zarządzanie swoimi wizytówkami ({/@andrea=przykład}).
Założenie konta pozwala na zarządzanie swoimi wizytówkami ({/@andrea=takimi jak ta}).
code:
action: 'Sprawdź'
invalid: 'Kod nieprawidłowy.'
@ -706,6 +706,12 @@ user:
change: 'Zmień'
deleteAccount: 'Usuń konto'
deleteAccountConfirm: 'Czy na pewno chcesz usunąć swoje konto? Ta operacja jest nieodwracalna!'
socialConnection:
list: 'Połączenia z social media'
connect: 'Połącz'
refresh: 'Odśwież'
disconnect: 'Rozłącz'
disconnectConfirm: 'Czy na pewno chcesz usunąć to połączenie? (Zawsze możesz logować się przez maila %email%)'
profile:
description: 'Opis'

View File

@ -15,9 +15,12 @@
"@nuxtjs/redirect-module": "^0.3.1",
"body-parser": "^1.19.0",
"canvas": "^2.6.1",
"cookie-parser": "^1.4.5",
"cookie-universal-nuxt": "^2.1.4",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.1",
"grant": "^5.4.5",
"js-base64": "^3.5.2",
"js-md5": "^0.7.3",
"jsonwebtoken": "^8.5.1",

View File

@ -11,11 +11,6 @@ export default ({app, store}) => {
}
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';
};

View File

@ -175,7 +175,7 @@
},
async save() {
this.saving = true;
const response = await this.$axios.$post(`/profile/save/${this.config.locale}`, {
await this.$axios.$post(`/profile/save/${this.config.locale}`, {
names: listToDict(this.names),
pronouns: listToDict(this.pronouns),
description: this.description,
@ -183,7 +183,7 @@
links: [...this.links],
flags: [...this.flags],
words: this.words.map(x => listToDict(x)),
}, { headers: this.$auth() });
});
this.saving = false;
this.$router.push(`/@${this.$user().username}`)
},

View File

@ -1,11 +1,22 @@
import express from 'express';
import authenticate from '../src/authenticate';
import dbConnection from './db';
import session from 'express-session';
import cookieParser from 'cookie-parser';
import grant from "grant";
import router from "./routes/user";
const app = express()
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(session({
secret: process.env.SECRET,
cookie: {
secure: process.env.NODE_ENV === 'production',
},
}));
app.use(async function (req, res, next) {
req.rawUser = authenticate(req);
@ -13,7 +24,9 @@ app.use(async function (req, res, next) {
req.admin = req.user && req.user.roles === 'admin';
req.db = await dbConnection();
next();
})
});
router.use(grant.express()(require('./social').default));
app.use(require('./routes/banner').default);

View File

@ -1,10 +1,11 @@
import { Router } from 'express';
import SQL from 'sql-template-strings';
import {ulid} from "ulid";
import {makeId} from "../../src/helpers";
import {buildDict, makeId} from "../../src/helpers";
import translations from "../translations";
import jwt from "../../src/jwt";
import mailer from "../../src/mailer";
import config from '../config';
const now = Math.floor(Date.now() / 1000);
@ -46,7 +47,7 @@ const invalidateAuthenticator = async (db, id) => {
}
const defaultUsername = async (db, email) => {
const base = email.substring(0, email.indexOf('@'))
const base = email.substring(0, email.includes('@') ? email.indexOf('@') : email.length)
.padEnd(4, '0')
.substring(0, 12)
.replace(new RegExp(`[^${USERNAME_CHARS}]`, 'g'), '_');
@ -62,12 +63,12 @@ const defaultUsername = async (db, email) => {
}
}
const issueAuthentication = async (db, user) => {
const fetchOrCreateUser = async (db, user) => {
let dbUser = await db.get(SQL`SELECT * FROM users WHERE email = ${normalise(user.email)}`);
if (!dbUser) {
dbUser = {
id: ulid(),
username: await defaultUsername(db, user.email),
username: await defaultUsername(db, user.name || user.email),
email: normalise(user.email),
roles: 'user',
avatarSource: null,
@ -76,12 +77,16 @@ const issueAuthentication = async (db, user) => {
VALUES (${dbUser.id}, ${dbUser.username}, ${dbUser.email}, ${dbUser.roles}, ${dbUser.avatarSource})`)
}
return {
token: jwt.sign({
...dbUser,
authenticated: true,
}),
};
return dbUser;
}
const issueAuthentication = async (db, user) => {
const dbUser = await fetchOrCreateUser(db, user);
return jwt.sign({
...dbUser,
authenticated: true,
});
}
const validateEmail = (email) => {
@ -150,7 +155,7 @@ router.post('/user/validate', async (req, res) => {
await invalidateAuthenticator(req.db, authenticator);
return res.json(await issueAuthentication(req.db, req.rawUser));
return res.json({token: await issueAuthentication(req.db, req.rawUser)});
});
router.post('/user/change-username', async (req, res) => {
@ -169,7 +174,7 @@ router.post('/user/change-username', async (req, res) => {
await req.db.get(SQL`UPDATE users SET username = ${req.body.username} WHERE email = ${normalise(req.user.email)}`);
return res.json(await issueAuthentication(req.db, req.user));
return res.json({token: await issueAuthentication(req.db, req.user)});
});
router.post('/user/change-email', async (req, res) => {
@ -218,7 +223,7 @@ router.post('/user/change-email', async (req, res) => {
await req.db.get(SQL`UPDATE users SET email = ${authenticator.payload.to} WHERE email = ${normalise(req.user.email)}`);
req.user.email = authenticator.payload.to;
return res.json(await issueAuthentication(req.db, req.user));
return res.json({token: await issueAuthentication(req.db, req.user)});
});
router.post('/user/delete', async (req, res) => {
@ -238,4 +243,108 @@ router.post('/user/delete', async (req, res) => {
return res.json(true);
});
const socialLoginHandlers = {
twitter(r) {
return {
id: r.profile.id_str,
email: r.profile.email,
name: r.profile.screen_name,
avatar: r.profile.profile_image_url_https.replace('_normal', '_400x400'),
access_token: r.access_token,
access_secret: r.access_secret,
}
},
facebook(r) {
console.log(r);
return {
id: r.profile.id,
email: r.profile.email,
name: r.profile.name,
avatar: r.profile.picture.data.url,
access_token: r.access_token,
access_secret: r.access_secret,
}
},
google(r) {
console.log(r);
return {
id: r.profile.sub,
email: r.profile.email_verified !== false ? r.profile.email : undefined,
name: r.profile.email,
avatar: r.profile.picture,
access_token: r.access_token,
access_secret: r.access_secret,
}
},
};
router.get('/user/social/:provider', async (req, res) => {
const payload = socialLoginHandlers[req.params.provider](req.session.grant.response)
const auth = await req.db.get(SQL`
SELECT * FROM authenticators
WHERE type = ${req.params.provider}
AND payload LIKE ${'{"id":"' + payload.id + '"%'}
AND (validUntil IS NULL OR validUntil > ${now})
`)
const user = auth ? await req.db.get(SQL`
SELECT * FROM users
WHERE id = ${auth.userId}
`) : req.user;
const dbUser = await fetchOrCreateUser(req.db, user || {
email: payload.email || `${payload.id}@${req.params.provider}.oauth`,
name: payload.name,
});
const token = jwt.sign({
...dbUser,
authenticated: true,
});
if (auth) {
await invalidateAuthenticator(req.db, auth.id);
}
await saveAuthenticator(req.db, req.params.provider, dbUser, payload);
return res.cookie('token', token).redirect('/' + config.user.route);
});
router.get('/user/social-connections', async (req, res) => {
if (!req.user) {
return res.status(401).json({error: 'Unauthorised'});
}
const authenticators = await req.db.all(SQL`
SELECT type, payload FROM authenticators
WHERE type IN (`.append(Object.keys(socialLoginHandlers).map(k => `'${k}'`).join(',')).append(SQL`)
AND userId = ${req.user.id}
AND (validUntil IS NULL OR validUntil > ${now})
`));
return res.json(buildDict(function* () {
for (let auth of authenticators) {
yield [auth.type, JSON.parse(auth.payload)];
}
}));
});
router.post('/user/social-connection/:provider/disconnect', async (req, res) => {
if (!req.user) {
return res.status(401).json({error: 'Unauthorised'});
}
const auth = await req.db.get(SQL`
SELECT id FROM authenticators
WHERE type = ${req.params.provider}
AND userId = ${req.user.id}
AND (validUntil IS NULL OR validUntil > ${now})
`)
await invalidateAuthenticator(req.db, auth.id)
return res.json('ok');
});
export default router;

27
server/social.js Normal file
View File

@ -0,0 +1,27 @@
export default {
defaults: {
origin: process.env.BASE_URL,
transport: 'session',
state: true,
prefix: '/api/connect',
scope: ['email'],
response: ['tokens', 'raw', 'profile'],
},
twitter: {
key: process.env.TWITTER_KEY,
secret: process.env.TWITTER_SECRET,
callback: '/api/user/social/twitter',
profile_url: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
},
facebook: {
key: process.env.FACEBOOK_KEY,
secret: process.env.FACEBOOK_SECRET,
callback: '/api/user/social/facebook',
profile_url: 'https://graph.facebook.com/me?fields=email,name,picture'
},
google: {
key: process.env.GOOGLE_KEY,
secret: process.env.GOOGLE_SECRET,
callback: '/api/user/social/google',
}
}

View File

@ -1,9 +1,13 @@
import jwt from './jwt';
export default ({headers: { authorization }}) => {
if (!authorization || !authorization.startsWith('Bearer ')) {
return null;
export default ({cookies, headers}) => {
if (headers.authorization && headers.authorization.startsWith('Bearer ')) {
return jwt.validate(headers.authorization.substring(7));
}
return jwt.validate(authorization.substring(7));
if (cookies.token && cookies.token !== 'null') {
return jwt.validate(cookies.token)
}
return null;
}

View File

@ -10,6 +10,12 @@ export const locales = {
// nl: { name: 'Nederlands', url: 'https://nl.pronouns.page' },
}
export const socialProviders = {
twitter: { name: 'Twitter' },
facebook: { name: 'Facebook' },
google: { name: 'Google' },
}
import templatesRaw from '../data/templates/templates.tsv';
export const templates = parseTemplates(templatesRaw);

126
yarn.lock
View File

@ -1891,6 +1891,16 @@ asn1.js@^4.0.0:
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
asn1.js@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
dependencies:
bn.js "^4.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
safer-buffer "^2.1.0"
asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@ -2828,11 +2838,24 @@ convert-source-map@^1.7.0:
dependencies:
safe-buffer "~5.1.1"
cookie-parser@^1.4.5:
version "1.4.5"
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49"
integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==
dependencies:
cookie "0.4.0"
cookie-signature "1.0.6"
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie-signature@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.1.0.tgz#cc94974f91fb9a9c1bb485e95fc2b7f4b120aff2"
integrity sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A==
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"
@ -2859,7 +2882,7 @@ 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:
cookie@^0.4.0, cookie@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
@ -3335,6 +3358,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
depd@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
des.js@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@ -3504,7 +3532,7 @@ electron-to-chromium@^1.3.488:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.500.tgz#ac082dd2279251b14f1a7f6af6fd16655cf3eb7b"
integrity sha512-Zz8BZh4Ssb/rZBaicqpi+GOQ0uu3y+24+MxBLCk0UYt8EGoZRP4cYzYHHwXGZfrSbCU4VDjbWN+Tg+TPgOUX6Q==
elliptic@^6.0.0, elliptic@^6.5.2:
elliptic@^6.0.0, elliptic@^6.5.2, elliptic@^6.5.3:
version "6.5.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
@ -3740,6 +3768,20 @@ expand-brackets@^2.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
express-session@^1.17.1:
version "1.17.1"
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.1.tgz#36ecbc7034566d38c8509885c044d461c11bf357"
integrity sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==
dependencies:
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~2.0.0"
on-headers "~1.0.2"
parseurl "~1.3.3"
safe-buffer "5.2.0"
uid-safe "~2.1.5"
express@^4.16.3, express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@ -4252,6 +4294,20 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
grant@^5.4.5:
version "5.4.5"
resolved "https://registry.yarnpkg.com/grant/-/grant-5.4.5.tgz#ab1d3001bf33392eb1c09266ff8bc91f6553cbe0"
integrity sha512-w1/mbzGEztHSpZIH96K0O43lgascuvP/jyiXpu6JsLQ4Hu7c3ZKb7KqDTQhRQdmJ+oZoyDffPK2reuVn6eU9Kw==
dependencies:
qs "^6.9.4"
request-compose "^2.1.0"
request-oauth "^1.0.0"
optionalDependencies:
cookie "^0.4.1"
cookie-signature "^1.1.0"
jwk-to-pem "^2.0.3"
jws "^4.0.0"
gzip-size@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"
@ -5148,6 +5204,24 @@ jwa@^1.4.1:
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jwa@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jwk-to-pem@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/jwk-to-pem/-/jwk-to-pem-2.0.4.tgz#774e168697a0b52054e8cfb0bcd57e0f4c398e2d"
integrity sha512-4CCK9UBHNWjWtfSHdyu3I6rA8vlN5cWqnVuwY0cOMyXtw6M1tP+yrM8GZpwk+P932Dc3cLag4d35B6CqyIf89A==
dependencies:
asn1.js "^5.3.0"
elliptic "^6.5.3"
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"
@ -5156,6 +5230,14 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
jws@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
dependencies:
jwa "^2.0.0"
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"
@ -6012,7 +6094,7 @@ nuxt@^2.14.7:
"@nuxt/telemetry" "^1.2.3"
"@nuxt/webpack" "2.14.7"
oauth-sign@~0.9.0:
oauth-sign@^0.9.0, oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
@ -7220,6 +7302,11 @@ qs@6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@^6.9.3, qs@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@ -7243,6 +7330,11 @@ querystring@0.2.0, querystring@^0.2.0:
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
random-bytes@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -7443,6 +7535,20 @@ repeat-string@^1.6.1:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
request-compose@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/request-compose/-/request-compose-2.1.0.tgz#2064e886a7745a6af425609aa116ebfeffc1792d"
integrity sha512-mIWvU9HA2whb/fHcqeQ0LQXAImCGISqUPyjuFF2rALhym2Fu4ebZGv7wxFA78rsJO/fn2OeEaK54TSjwSwRAFw==
request-oauth@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/request-oauth/-/request-oauth-1.0.0.tgz#c8ebe047fb4ff4d5aa4ddb33e98b9ac74c625674"
integrity sha512-wsDzIq1Qu2itLDlcpFph8xh5Q+FVyUj4os5zdQTlZL/JvZYF/qOyaawVPsxxhDG4QwCB3tzSFprj6dkjqR+e8w==
dependencies:
oauth-sign "^0.9.0"
qs "^6.9.3"
uuid "^3.4.0"
request@^2.87.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
@ -7583,6 +7689,11 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@ -8482,6 +8593,13 @@ uglify-js@^3.5.1:
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.0.tgz#397a7e6e31ce820bfd1cb55b804ee140c587a9e7"
integrity sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA==
uid-safe@~2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==
dependencies:
random-bytes "~1.0.0"
ulid@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f"
@ -8680,7 +8798,7 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@^3.3.2:
uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==