#291 [auth][mfa] MFA
This commit is contained in:
parent
feb08abfe9
commit
113b040f25
|
@ -139,6 +139,9 @@
|
||||||
<SocialConnection :provider="provider" :providerOptions="providerOptions" :connection="socialConnections[provider]"
|
<SocialConnection :provider="provider" :providerOptions="providerOptions" :connection="socialConnections[provider]"
|
||||||
@disconnected="socialConnections[provider] = undefined" @setAvatar="setAvatar"/>
|
@disconnected="socialConnections[provider] = undefined" @setAvatar="setAvatar"/>
|
||||||
</li>
|
</li>
|
||||||
|
<li :class="['list-group-item', $user().mfa ? 'profile-current' : '']">
|
||||||
|
<MfaConnection/>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Loading>
|
</Loading>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<section>
|
<section>
|
||||||
<Alert type="danger" :message="error"/>
|
<Alert type="danger" :message="error"/>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div v-if="token === null">
|
<div v-if="token === null">
|
||||||
<p v-if="$te('user.login.help')">
|
<p v-if="$te('user.login.help')">
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center">
|
||||||
|
<span class="my-2 me-3 text-nowrap">
|
||||||
|
<Icon v="mobile"/>
|
||||||
|
<T>user.mfa.header</T>
|
||||||
|
</span>
|
||||||
|
<Spinner v-if="requesting" size="2rem"/>
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="$user().mfa">
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<Icon v="shield-check"/>
|
||||||
|
<T>user.mfa.enabled</T>
|
||||||
|
</span>
|
||||||
|
<button class="badge bg-light text-dark border" @click.prevent="disable">
|
||||||
|
<Icon v="unlink"/>
|
||||||
|
<T>user.mfa.disable</T>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!secret">
|
||||||
|
<button class="badge bg-light text-dark border" @click="getLink">
|
||||||
|
<Icon v="link"/>
|
||||||
|
<T>user.mfa.enable</T>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="recoveryCodes === null" class="text-center">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<Icon v="info-circle"/>
|
||||||
|
<T>user.mfa.init</T>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<img :src="secret.qr" alt="QR Code" class="mw-100"/>
|
||||||
|
<br/>
|
||||||
|
<small>{{secret.base32}}</small>
|
||||||
|
</p>
|
||||||
|
<form @submit.prevent="init" 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"
|
||||||
|
ref="code"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-outline-primary">
|
||||||
|
<Icon v="key"/>
|
||||||
|
<T>user.code.action</T>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p>
|
||||||
|
<Icon v="info-circle"/>
|
||||||
|
<T>user.mfa.recovery.save</T>
|
||||||
|
</p>
|
||||||
|
<ul class="mb-4">
|
||||||
|
<li v-for="code in recoveryCodes">{{code}}</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-primary" @click="$user().mfa = true">
|
||||||
|
<Icon v="shield-check"/>
|
||||||
|
<T>user.mfa.recovery.saved</T>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
secret: null,
|
||||||
|
code: '',
|
||||||
|
validation: null,
|
||||||
|
recoveryCodes: null,
|
||||||
|
requesting: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getLink() {
|
||||||
|
this.requesting = true;
|
||||||
|
this.secret = await this.$axios.$get('/mfa/get-url');
|
||||||
|
this.requesting = false;
|
||||||
|
},
|
||||||
|
async init() {
|
||||||
|
this.requesting = true;
|
||||||
|
try {
|
||||||
|
this.recoveryCodes = await this.$axios.$post('/mfa/init', {
|
||||||
|
secret: this.secret.base32,
|
||||||
|
token: this.code,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
await this.$alert(this.$t('user.code.invalid'), 'danger');
|
||||||
|
} finally {
|
||||||
|
this.requesting = false;
|
||||||
|
this.code = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async disable() {
|
||||||
|
await this.$confirm(this.$t('user.mfa.disableConfirm'), 'danger');
|
||||||
|
await this.$post('/mfa/disable');
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<Alert type="danger" :message="error"/>
|
||||||
|
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="h4">
|
||||||
|
<Icon v="mobile"/>
|
||||||
|
<T>user.mfa.header</T>
|
||||||
|
</p>
|
||||||
|
<form @submit.prevent="validate" :disabled="saving">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input v-if="!recovery" 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"
|
||||||
|
/>
|
||||||
|
<input v-else type="text" class="form-control text-center" v-model="recoveryCode"
|
||||||
|
:placeholder="$t('user.mfa.recovery.header')" autofocus required minlength="0" maxlength="24"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
<Icon v="key"/>
|
||||||
|
<T>user.code.action</T>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer small d-flex justify-content-around">
|
||||||
|
<a href="#" @click.prevent="recoverySwitch">
|
||||||
|
<Icon v="ambulance"/>
|
||||||
|
<T>user.mfa.recovery.enter</T>
|
||||||
|
</a>
|
||||||
|
<a href="#" @click.prevent="cancel">
|
||||||
|
<Icon v="sign-out"/>
|
||||||
|
<T>user.mfa.cancel</T>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
recoveryCode: '',
|
||||||
|
recovery: false,
|
||||||
|
saving: false,
|
||||||
|
error: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.focus();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async validate() {
|
||||||
|
if (this.saving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.error = '';
|
||||||
|
this.saving = true;
|
||||||
|
try {
|
||||||
|
const res = await this.$axios.$post(`/mfa/validate`, {
|
||||||
|
code: this.recovery ? this.recoveryCode : this.code,
|
||||||
|
recovery: this.recovery,
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
authorization: 'Bearer ' + this.preToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (res.error) {
|
||||||
|
this.error = res.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$store.commit('setToken', res.token);
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
this.code = '';
|
||||||
|
this.recoveryCode = '';
|
||||||
|
this.recovery = false;
|
||||||
|
this.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.$store.commit('cancelMfa');
|
||||||
|
},
|
||||||
|
recoverySwitch() {
|
||||||
|
this.recovery = !this.recovery;
|
||||||
|
this.code = '';
|
||||||
|
this.recoveryCode = '';
|
||||||
|
this.focus();
|
||||||
|
},
|
||||||
|
focus() {
|
||||||
|
this.$nextTick(() => this.$el.querySelector('input').focus());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState([
|
||||||
|
'preToken',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -514,6 +514,23 @@ user:
|
||||||
refresh: 'Refresh'
|
refresh: 'Refresh'
|
||||||
disconnect: 'Disconnect'
|
disconnect: 'Disconnect'
|
||||||
disconnectConfirm: 'Are you sure you want to remove this connection? (You can always log in using email %email%)'
|
disconnectConfirm: 'Are you sure you want to remove this connection? (You can always log in using email %email%)'
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Description'
|
description: 'Description'
|
||||||
|
|
|
@ -412,6 +412,24 @@ user:
|
||||||
refresh: 'Aktualisieren'
|
refresh: 'Aktualisieren'
|
||||||
disconnect: 'Verbindung trennen'
|
disconnect: 'Verbindung trennen'
|
||||||
disconnectConfirm: 'Bist du sicher, dass du die Verbindung trennen möchtest? (Du kannst dich jederzeit mit der E-Mail %email% anmelden)'
|
disconnectConfirm: 'Bist du sicher, dass du die Verbindung trennen möchtest? (Du kannst dich jederzeit mit der E-Mail %email% anmelden)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Beschreibung'
|
description: 'Beschreibung'
|
||||||
|
|
|
@ -515,6 +515,23 @@ user:
|
||||||
refresh: 'Refresh'
|
refresh: 'Refresh'
|
||||||
disconnect: 'Disconnect'
|
disconnect: 'Disconnect'
|
||||||
disconnectConfirm: 'Are you sure you want to remove this connection? (You can always log in using email %email%)'
|
disconnectConfirm: 'Are you sure you want to remove this connection? (You can always log in using email %email%)'
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Description'
|
description: 'Description'
|
||||||
|
|
|
@ -425,6 +425,24 @@ user:
|
||||||
refresh: 'Actualizar'
|
refresh: 'Actualizar'
|
||||||
disconnect: 'Desconectar'
|
disconnect: 'Desconectar'
|
||||||
disconnectConfirm: '¿Confirmas que quieres eliminar esta conexión? (Siempre puedes iniciar sesión usando el correo electrónico %email%)'
|
disconnectConfirm: '¿Confirmas que quieres eliminar esta conexión? (Siempre puedes iniciar sesión usando el correo electrónico %email%)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Descripción'
|
description: 'Descripción'
|
||||||
|
|
|
@ -418,6 +418,24 @@ user:
|
||||||
refresh: 'Rafraîchir'
|
refresh: 'Rafraîchir'
|
||||||
disconnect: 'Déconnecter'
|
disconnect: 'Déconnecter'
|
||||||
disconnectConfirm: 'Êtes-vous sûr·e de vouloir retirer cette connexion ? (Vous pouvez toujours vous connecter en utilisant l’adresse mail %email%)'
|
disconnectConfirm: 'Êtes-vous sûr·e de vouloir retirer cette connexion ? (Vous pouvez toujours vous connecter en utilisant l’adresse mail %email%)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Description'
|
description: 'Description'
|
||||||
|
|
|
@ -424,6 +424,24 @@ user:
|
||||||
refresh: 'Atualizar'
|
refresh: 'Atualizar'
|
||||||
disconnect: 'Desconectar'
|
disconnect: 'Desconectar'
|
||||||
disconnectConfirm: 'Confirma que quer excluir esta conexão? (Sempre pode iniciar sessão usando seu endereço %email%)'
|
disconnectConfirm: 'Confirma que quer excluir esta conexão? (Sempre pode iniciar sessão usando seu endereço %email%)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Descrição'
|
description: 'Descrição'
|
||||||
|
|
|
@ -428,6 +428,24 @@ user:
|
||||||
refresh: '更新'
|
refresh: '更新'
|
||||||
disconnect: '切断'
|
disconnect: '切断'
|
||||||
disconnectConfirm: 'この接続を削除してもよろしいですか?まだメールアドレスでログインできます。(%email%)'
|
disconnectConfirm: 'この接続を削除してもよろしいですか?まだメールアドレスでログインできます。(%email%)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: '記述'
|
description: '記述'
|
||||||
|
|
|
@ -417,6 +417,24 @@ user:
|
||||||
refresh: 'Vernieuw'
|
refresh: 'Vernieuw'
|
||||||
disconnect: 'Ontkoppel'
|
disconnect: 'Ontkoppel'
|
||||||
disconnectConfirm: 'Weet je zeker dat je deze koppeling wil verwijderen? (Je kunt altijd inloggen met de email %email%)'
|
disconnectConfirm: 'Weet je zeker dat je deze koppeling wil verwijderen? (Je kunt altijd inloggen met de email %email%)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Omschrijving'
|
description: 'Omschrijving'
|
||||||
|
|
|
@ -420,6 +420,24 @@ user:
|
||||||
refresh: 'Last inn på nytt'
|
refresh: 'Last inn på nytt'
|
||||||
disconnect: 'Koble fra'
|
disconnect: 'Koble fra'
|
||||||
disconnectConfirm: 'Er du sikker på at du vil kobla fra? (Du kan alltid logge inn via email %email%)'
|
disconnectConfirm: 'Er du sikker på at du vil kobla fra? (Du kan alltid logge inn via email %email%)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Beskrivelse'
|
description: 'Beskrivelse'
|
||||||
|
|
|
@ -1212,6 +1212,23 @@ user:
|
||||||
refresh: 'Odśwież'
|
refresh: 'Odśwież'
|
||||||
disconnect: 'Rozłącz'
|
disconnect: 'Rozłącz'
|
||||||
disconnectConfirm: 'Czy na pewno chcesz usunąć to połączenie? (Zawsze możesz logować się przez maila %email%)'
|
disconnectConfirm: 'Czy na pewno chcesz usunąć to połączenie? (Zawsze możesz logować się przez maila %email%)'
|
||||||
|
mfa:
|
||||||
|
header: 'Uwierzytelnianie wieloskładnikowe (MFA)'
|
||||||
|
init: >
|
||||||
|
Zeskanuj poniższy kod QR (lub wklej kod tekstowy pod spodem) do swojej apki TOTP (np. {https://authy.com/=Authy}),
|
||||||
|
a następnie wpisz wstępny token, jaki zostanie wygenerowany.
|
||||||
|
recovery:
|
||||||
|
header: 'Kod odzyskiwania'
|
||||||
|
save: >
|
||||||
|
Zapisz poniższe kody odzyskiwania w bezpiecznym miejscu.
|
||||||
|
Będziesz mogłx ich użyć, aby obejść MFA na wypadek utraty urządzenia uwierzytelniającego.
|
||||||
|
saved: 'OK, zapisane!'
|
||||||
|
enter: 'Wpisz kod odzyskiwania'
|
||||||
|
cancel: 'Anuluj logowanie'
|
||||||
|
enabled: 'Włączone'
|
||||||
|
enable: 'Włącz MFA'
|
||||||
|
disable: 'Wyłącz MFA'
|
||||||
|
disableConfirm: 'Czy na pewno chcesz wyłączyć MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Opis'
|
description: 'Opis'
|
||||||
|
|
|
@ -420,6 +420,24 @@ user:
|
||||||
refresh: 'Atualizar'
|
refresh: 'Atualizar'
|
||||||
disconnect: 'Desconectar'
|
disconnect: 'Desconectar'
|
||||||
disconnectConfirm: 'Confirma que quer excluir esta conexão? (Sempre pode iniciar sessão usando seu endereço %email%)'
|
disconnectConfirm: 'Confirma que quer excluir esta conexão? (Sempre pode iniciar sessão usando seu endereço %email%)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Descrição'
|
description: 'Descrição'
|
||||||
|
|
|
@ -453,6 +453,24 @@ user:
|
||||||
refresh: 'Обновить'
|
refresh: 'Обновить'
|
||||||
disconnect: 'Отсоединить'
|
disconnect: 'Отсоединить'
|
||||||
disconnectConfirm: 'Вы уверены, что хотите отсоединить привязанную социальную сеть? (Вы всегда можете войти в аккаунт, используя почту %email%)'
|
disconnectConfirm: 'Вы уверены, что хотите отсоединить привязанную социальную сеть? (Вы всегда можете войти в аккаунт, используя почту %email%)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'Описание'
|
description: 'Описание'
|
||||||
|
|
|
@ -420,6 +420,24 @@ user:
|
||||||
refresh: 'Refresh'
|
refresh: 'Refresh'
|
||||||
disconnect: 'Disconnect'
|
disconnect: 'Disconnect'
|
||||||
disconnectConfirm: 'Are you sure you want to remove this connection? (You can always log in using email %email%)'
|
disconnectConfirm: 'Are you sure you want to remove this connection? (You can always log in using email %email%)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: 'באַשרײַבונג'
|
description: 'באַשרײַבונג'
|
||||||
|
|
|
@ -405,6 +405,24 @@ user:
|
||||||
refresh: '祓飾'
|
refresh: '祓飾'
|
||||||
disconnect: '掉線'
|
disconnect: '掉線'
|
||||||
disconnectConfirm: '確定要刪除此連接嗎? (您始終可以使用電子郵件登錄 %email%)'
|
disconnectConfirm: '確定要刪除此連接嗎? (您始終可以使用電子郵件登錄 %email%)'
|
||||||
|
# TODO
|
||||||
|
mfa:
|
||||||
|
header: 'Multi-factor authentication'
|
||||||
|
init: >
|
||||||
|
Scan this QR code (or enter the text code below) in your TOTP authenticator app (eg. {https://authy.com/=Authy})
|
||||||
|
and then enter the initial token that gets generated.
|
||||||
|
recovery:
|
||||||
|
header: 'Recovery code'
|
||||||
|
save: >
|
||||||
|
Save the following recovery codes in a safe place.
|
||||||
|
You'll be able to use them to bypass MFA in case you ever lose your authentication device.
|
||||||
|
saved: 'OK, I''ve saved them!'
|
||||||
|
enter: 'Enter recovery code'
|
||||||
|
cancel: 'Cancel login'
|
||||||
|
enabled: 'Enabled'
|
||||||
|
enable: 'Enable MFA'
|
||||||
|
disable: 'Disable MFA'
|
||||||
|
disableConfirm: 'Are you sure you want to disable MFA?'
|
||||||
|
|
||||||
profile:
|
profile:
|
||||||
description: '傳記'
|
description: '傳記'
|
||||||
|
|
|
@ -40,8 +40,10 @@
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"nuxt": "^2.15.2",
|
"nuxt": "^2.15.2",
|
||||||
"pageres": "^6.2.3",
|
"pageres": "^6.2.3",
|
||||||
|
"qrcode": "^1.5.0",
|
||||||
"rtlcss": "^3.1.2",
|
"rtlcss": "^3.1.2",
|
||||||
"sha1": "^1.1.1",
|
"sha1": "^1.1.1",
|
||||||
|
"speakeasy": "^2.0.0",
|
||||||
"sql-template-strings": "^2.2.2",
|
"sql-template-strings": "^2.2.2",
|
||||||
"sqlite": "^4.0.12",
|
"sqlite": "^4.0.12",
|
||||||
"sqlite3": "^5.0.0",
|
"sqlite3": "^5.0.0",
|
||||||
|
|
|
@ -6,14 +6,21 @@
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<Account v-if="$user()"/>
|
<Account v-if="$user()"/>
|
||||||
|
<MfaValidation v-else-if="preToken"/>
|
||||||
<Login v-else/>
|
<Login v-else/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { head } from "../src/helpers";
|
import { head } from "../src/helpers";
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapState([
|
||||||
|
'preToken',
|
||||||
|
]),
|
||||||
|
},
|
||||||
head() {
|
head() {
|
||||||
return head({
|
return head({
|
||||||
title: this.$t('user.headerLong'),
|
title: this.$t('user.headerLong'),
|
||||||
|
|
|
@ -11,6 +11,7 @@ import cookieSettings from "../src/cookieSettings";
|
||||||
import SQL from "sql-template-strings";
|
import SQL from "sql-template-strings";
|
||||||
|
|
||||||
global.config = loadSuml('config');
|
global.config = loadSuml('config');
|
||||||
|
global.translations = loadSuml('translations');
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
app.enable('trust proxy')
|
app.enable('trust proxy')
|
||||||
|
@ -92,6 +93,7 @@ app.use(require('./routes/banner').default);
|
||||||
app.use(require('./routes/user').default);
|
app.use(require('./routes/user').default);
|
||||||
app.use(require('./routes/profile').default);
|
app.use(require('./routes/profile').default);
|
||||||
app.use(require('./routes/admin').default);
|
app.use(require('./routes/admin').default);
|
||||||
|
app.use(require('./routes/mfa').default);
|
||||||
|
|
||||||
app.use(require('./routes/pronouns').default);
|
app.use(require('./routes/pronouns').default);
|
||||||
app.use(require('./routes/sources').default);
|
app.use(require('./routes/sources').default);
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import {handleErrorAsync} from "../../src/helpers";
|
||||||
|
import speakeasy from 'speakeasy';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
import {saveAuthenticator, findAuthenticatorsByUser, invalidateAuthenticator, issueAuthentication, normalise} from './user';
|
||||||
|
import cookieSettings from "../../src/cookieSettings";
|
||||||
|
|
||||||
|
|
||||||
|
export const addMfaInfo = async (db, user, guard = false) => {
|
||||||
|
const auths = await findAuthenticatorsByUser(db, user, 'mfa_secret');
|
||||||
|
if (auths.length) {
|
||||||
|
user.mfa = true;
|
||||||
|
if (guard) {
|
||||||
|
user.mfaRequired = true;
|
||||||
|
user.authenticated = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user.mfa = false;
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
const disableMfa = async (db, user) => {
|
||||||
|
for (let authenticator of [
|
||||||
|
...await findAuthenticatorsByUser(db, user, 'mfa_secret'),
|
||||||
|
...await findAuthenticatorsByUser(db, user, 'mfa_recovery'),
|
||||||
|
]) {
|
||||||
|
await invalidateAuthenticator(db, authenticator.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/mfa/get-url', handleErrorAsync(async (req, res) => {
|
||||||
|
if (!req.user || req.user.mfa) {
|
||||||
|
return res.status(401).json({error: 'Unauthorised'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = speakeasy.generateSecret({
|
||||||
|
length: 16,
|
||||||
|
name: global.translations.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
secret.qr = await QRCode.toDataURL(secret.otpauth_url, {
|
||||||
|
margin: 2,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json(secret);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/mfa/init', handleErrorAsync(async (req, res) => {
|
||||||
|
if (!req.user || req.user.mfa) {
|
||||||
|
return res.status(401).json({error: 'Unauthorised'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const verified = speakeasy.totp.verify({
|
||||||
|
secret: req.body.secret,
|
||||||
|
encoding: 'base32',
|
||||||
|
token: req.body.token
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!verified) {
|
||||||
|
return res.status(400).json({error: 'Invalid token'});
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveAuthenticator(req.db, 'mfa_secret', req.user, req.body.secret);
|
||||||
|
const recoveryCodes = [];
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const code = speakeasy.generateSecretASCII(24);
|
||||||
|
recoveryCodes.push(code);
|
||||||
|
await saveAuthenticator(req.db, 'mfa_recovery', req.user, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await issueAuthentication(req.db, req.user);
|
||||||
|
|
||||||
|
return res.cookie('token', token, cookieSettings).json(recoveryCodes);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/mfa/validate', handleErrorAsync(async (req, res) => {
|
||||||
|
if (!req.rawUser || !req.rawUser.mfaRequired) {
|
||||||
|
return res.json({error: 'user.tokenExpired'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.body.recovery) {
|
||||||
|
for (let authenticator of await findAuthenticatorsByUser(req.db, req.rawUser, 'mfa_recovery')) {
|
||||||
|
if (authenticator.payload === req.body.code.trim()) {
|
||||||
|
await disableMfa(req.db, req.rawUser);
|
||||||
|
|
||||||
|
const token = await issueAuthentication(req.db, req.rawUser, true, false, { mfa: false, mfaRequired: false });
|
||||||
|
|
||||||
|
return res.cookie('token', token, cookieSettings).json({token: token});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({error: 'user.code.invalid'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticator = (await findAuthenticatorsByUser(req.db, req.rawUser, 'mfa_secret'))[0];
|
||||||
|
|
||||||
|
const tokenValidates = speakeasy.totp.verify({
|
||||||
|
secret: authenticator.payload,
|
||||||
|
encoding: 'base32',
|
||||||
|
token: normalise(req.body.code),
|
||||||
|
window: 6
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tokenValidates) {
|
||||||
|
return res.json({error: 'user.code.invalid'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await issueAuthentication(req.db, req.rawUser, true, false, { mfaRequired: false });
|
||||||
|
|
||||||
|
return res.cookie('token', token, cookieSettings).json({token: token});
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/mfa/disable', handleErrorAsync(async (req, res) => {
|
||||||
|
if (!req.user || !req.user.mfa) {
|
||||||
|
return res.status(401).json({error: 'Unauthorised'});
|
||||||
|
}
|
||||||
|
|
||||||
|
await disableMfa(req.db, req.user);
|
||||||
|
|
||||||
|
const token = await issueAuthentication(req.db, req.user);
|
||||||
|
|
||||||
|
return res.cookie('token', token, cookieSettings).json({token: token});
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default router;
|
|
@ -10,13 +10,14 @@ import { config as socialLoginConfig, handlers as socialLoginHandlers } from '..
|
||||||
import cookieSettings from "../../src/cookieSettings";
|
import cookieSettings from "../../src/cookieSettings";
|
||||||
import {validateCaptcha} from "../captcha";
|
import {validateCaptcha} from "../captcha";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
import {addMfaInfo} from './mfa';
|
||||||
|
|
||||||
const config = loadSuml('config');
|
const config = loadSuml('config');
|
||||||
const translations = loadSuml('translations');
|
const translations = loadSuml('translations');
|
||||||
|
|
||||||
const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';
|
const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';
|
||||||
|
|
||||||
const normalise = s => s.trim().toLowerCase();
|
export const normalise = s => s.trim().toLowerCase();
|
||||||
|
|
||||||
const isSpam = (email) => {
|
const isSpam = (email) => {
|
||||||
const noDots = email.replace(/\./g, '');
|
const noDots = email.replace(/\./g, '');
|
||||||
|
@ -32,7 +33,7 @@ const replaceExtension = username => username
|
||||||
.replace(/\.$/, '')
|
.replace(/\.$/, '')
|
||||||
;
|
;
|
||||||
|
|
||||||
const saveAuthenticator = async (db, type, user, payload, validForMinutes = null) => {
|
export const saveAuthenticator = async (db, type, user, payload, validForMinutes = null) => {
|
||||||
const id = ulid();
|
const id = ulid();
|
||||||
await db.get(SQL`INSERT INTO authenticators (id, userId, type, payload, validUntil) VALUES (
|
await db.get(SQL`INSERT INTO authenticators (id, userId, type, payload, validUntil) VALUES (
|
||||||
${id},
|
${id},
|
||||||
|
@ -44,7 +45,7 @@ const saveAuthenticator = async (db, type, user, payload, validForMinutes = null
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const findAuthenticator = async (db, id, type) => {
|
export const findAuthenticatorById = async (db, id, type) => {
|
||||||
const authenticator = await db.get(SQL`SELECT * FROM authenticators
|
const authenticator = await db.get(SQL`SELECT * FROM authenticators
|
||||||
WHERE id = ${id}
|
WHERE id = ${id}
|
||||||
AND type = ${type}
|
AND type = ${type}
|
||||||
|
@ -58,6 +59,21 @@ const findAuthenticator = async (db, id, type) => {
|
||||||
return authenticator
|
return authenticator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const findAuthenticatorsByUser = async (db, user, type) => {
|
||||||
|
const authenticators = await db.all(SQL`
|
||||||
|
SELECT * FROM authenticators
|
||||||
|
WHERE userId = ${user.id}
|
||||||
|
AND type = ${type}
|
||||||
|
AND (validUntil IS NULL OR validUntil > ${now()})
|
||||||
|
`);
|
||||||
|
|
||||||
|
return authenticators.map(a => {
|
||||||
|
a.payload = JSON.parse(a.payload);
|
||||||
|
|
||||||
|
return a;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const findLatestEmailAuthenticator = async (db, email, type) => {
|
const findLatestEmailAuthenticator = async (db, email, type) => {
|
||||||
const authenticator = await db.get(SQL`SELECT * FROM authenticators
|
const authenticator = await db.get(SQL`SELECT * FROM authenticators
|
||||||
WHERE payload LIKE ${'%"email":"' + email + '"%'}
|
WHERE payload LIKE ${'%"email":"' + email + '"%'}
|
||||||
|
@ -73,7 +89,7 @@ const findLatestEmailAuthenticator = async (db, email, type) => {
|
||||||
return authenticator
|
return authenticator
|
||||||
}
|
}
|
||||||
|
|
||||||
const invalidateAuthenticator = async (db, id) => {
|
export const invalidateAuthenticator = async (db, id) => {
|
||||||
await db.get(SQL`UPDATE authenticators
|
await db.get(SQL`UPDATE authenticators
|
||||||
SET validUntil = ${now()}
|
SET validUntil = ${now()}
|
||||||
WHERE id = ${id}
|
WHERE id = ${id}
|
||||||
|
@ -122,13 +138,31 @@ const fetchOrCreateUser = async (db, user, avatarSource = 'gravatar') => {
|
||||||
return dbUser;
|
return dbUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const issueAuthentication = async (db, user) => {
|
export const issueAuthentication = async (db, user, fetch = true, guardMfa = false, extend = undefined) => {
|
||||||
const dbUser = await fetchOrCreateUser(db, user);
|
if (fetch) {
|
||||||
|
user = await fetchOrCreateUser(db, user);
|
||||||
|
}
|
||||||
|
|
||||||
return jwt.sign({
|
if (user.mfa === undefined && user.id) {
|
||||||
...dbUser,
|
user = await addMfaInfo(db, user, guardMfa);
|
||||||
authenticated: true,
|
}
|
||||||
});
|
|
||||||
|
if (!user.mfaRequired) {
|
||||||
|
user.authenticated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.avatar = await avatar(db, user);
|
||||||
|
delete user.suspiciousChecked;
|
||||||
|
delete user.bannedBy;
|
||||||
|
|
||||||
|
if (extend) {
|
||||||
|
user = {
|
||||||
|
...user,
|
||||||
|
...extend,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwt.sign(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateEmail = async (email) => {
|
const validateEmail = async (email) => {
|
||||||
|
@ -172,7 +206,7 @@ const reloadUser = async (req, res, next) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbUser = await req.db.get(SQL`SELECT * FROM users WHERE id = ${req.user.id}`);
|
let dbUser = await req.db.get(SQL`SELECT * FROM users WHERE id = ${req.user.id}`);
|
||||||
|
|
||||||
if (!dbUser) {
|
if (!dbUser) {
|
||||||
res.clearCookie('token');
|
res.clearCookie('token');
|
||||||
|
@ -180,6 +214,8 @@ const reloadUser = async (req, res, next) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbUser = await addMfaInfo(req.db, dbUser);
|
||||||
|
|
||||||
await req.db.get(SQL`UPDATE users SET lastActive = ${+new Date} WHERE id = ${req.user.id}`);
|
await req.db.get(SQL`UPDATE users SET lastActive = ${+new Date} WHERE id = ${req.user.id}`);
|
||||||
|
|
||||||
if (req.user.username !== dbUser.username
|
if (req.user.username !== dbUser.username
|
||||||
|
@ -187,15 +223,12 @@ const reloadUser = async (req, res, next) => {
|
||||||
|| req.user.roles !== dbUser.roles
|
|| req.user.roles !== dbUser.roles
|
||||||
|| req.user.avatarSource !== dbUser.avatarSource
|
|| req.user.avatarSource !== dbUser.avatarSource
|
||||||
|| req.user.bannedReason !== dbUser.bannedReason
|
|| req.user.bannedReason !== dbUser.bannedReason
|
||||||
|
|| req.user.mfa !== dbUser.mfa
|
||||||
) {
|
) {
|
||||||
const newUser = {
|
const token = await issueAuthentication(req.db, dbUser, false);
|
||||||
...dbUser,
|
|
||||||
authenticated: true,
|
|
||||||
avatar: await avatar(req.db, dbUser),
|
|
||||||
};
|
|
||||||
const token = jwt.sign(newUser);
|
|
||||||
res.cookie('token', token, cookieSettings);
|
res.cookie('token', token, cookieSettings);
|
||||||
req.user = {...req.user, ...newUser};
|
req.rawUser = jwt.validate(token);
|
||||||
|
req.user = req.rawUser;
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
@ -278,7 +311,7 @@ router.post('/user/validate', handleErrorAsync(async (req, res) => {
|
||||||
return res.json({error: 'user.tokenExpired'});
|
return res.json({error: 'user.tokenExpired'});
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticator = await findAuthenticator(req.db, req.rawUser.codeKey, 'email');
|
const authenticator = await findAuthenticatorById(req.db, req.rawUser.codeKey, 'email');
|
||||||
if (!authenticator) {
|
if (!authenticator) {
|
||||||
return res.json({error: 'user.tokenExpired'});
|
return res.json({error: 'user.tokenExpired'});
|
||||||
}
|
}
|
||||||
|
@ -289,7 +322,7 @@ router.post('/user/validate', handleErrorAsync(async (req, res) => {
|
||||||
|
|
||||||
await invalidateAuthenticator(req.db, authenticator);
|
await invalidateAuthenticator(req.db, authenticator);
|
||||||
|
|
||||||
return res.json({token: await issueAuthentication(req.db, req.rawUser)});
|
return res.json({token: await issueAuthentication(req.db, req.rawUser, true, true)});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
router.post('/user/change-username', handleErrorAsync(async (req, res) => {
|
router.post('/user/change-username', handleErrorAsync(async (req, res) => {
|
||||||
|
@ -347,7 +380,7 @@ router.post('/user/change-email', handleErrorAsync(async (req, res) => {
|
||||||
return res.json({ authId });
|
return res.json({ authId });
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticator = await findAuthenticator(req.db, req.body.authId, 'changeEmail');
|
const authenticator = await findAuthenticatorById(req.db, req.body.authId, 'changeEmail');
|
||||||
if (!authenticator) {
|
if (!authenticator) {
|
||||||
return res.json({error: 'user.tokenExpired'});
|
return res.json({error: 'user.tokenExpired'});
|
||||||
}
|
}
|
||||||
|
@ -424,10 +457,7 @@ router.get('/user/social/:provider', handleErrorAsync(async (req, res) => {
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
}, req.params.provider);
|
}, req.params.provider);
|
||||||
|
|
||||||
const token = jwt.sign({
|
const token = await issueAuthentication(req.db, dbUser, false, true);
|
||||||
...dbUser,
|
|
||||||
authenticated: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (auth) {
|
if (auth) {
|
||||||
await invalidateAuthenticator(req.db, auth.id);
|
await invalidateAuthenticator(req.db, auth.id);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import jwt from 'jsonwebtoken';
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
token: null,
|
token: null,
|
||||||
user: null,
|
user: null,
|
||||||
|
preToken: null,
|
||||||
spelling: 'traditional',
|
spelling: 'traditional',
|
||||||
darkMode: false,
|
darkMode: false,
|
||||||
})
|
})
|
||||||
|
@ -27,7 +28,12 @@ export const mutations = {
|
||||||
user = null;
|
user = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user && user.mfaRequired) {
|
||||||
|
state.preToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
if (user && user.authenticated) {
|
if (user && user.authenticated) {
|
||||||
|
state.preToken = null;
|
||||||
state.token = token;
|
state.token = token;
|
||||||
state.user = user;
|
state.user = user;
|
||||||
return;
|
return;
|
||||||
|
@ -36,6 +42,9 @@ export const mutations = {
|
||||||
state.token = null;
|
state.token = null;
|
||||||
state.user = null;
|
state.user = null;
|
||||||
},
|
},
|
||||||
|
cancelMfa(state) {
|
||||||
|
state.preToken = null;
|
||||||
|
},
|
||||||
setSpelling(state, spelling) {
|
setSpelling(state, spelling) {
|
||||||
state.spelling = spelling;
|
state.spelling = spelling;
|
||||||
},
|
},
|
||||||
|
|
85
yarn.lock
85
yarn.lock
|
@ -2158,6 +2158,11 @@ balanced-match@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||||
|
|
||||||
|
base32.js@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.0.1.tgz#d045736a57b1f6c139f0c7df42518a84e91bb2ba"
|
||||||
|
integrity sha1-0EVzalex9sE58MffQlGKhOkbsro=
|
||||||
|
|
||||||
base64-js@^1.0.2, base64-js@^1.3.1:
|
base64-js@^1.0.2, base64-js@^1.3.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
|
@ -2815,6 +2820,15 @@ cliui@^4.0.0:
|
||||||
strip-ansi "^4.0.0"
|
strip-ansi "^4.0.0"
|
||||||
wrap-ansi "^2.0.0"
|
wrap-ansi "^2.0.0"
|
||||||
|
|
||||||
|
cliui@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
|
||||||
|
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
|
||||||
|
dependencies:
|
||||||
|
string-width "^4.2.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
wrap-ansi "^6.2.0"
|
||||||
|
|
||||||
clone-deep@^4.0.1:
|
clone-deep@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
|
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
|
||||||
|
@ -3649,6 +3663,11 @@ diffie-hellman@^5.0.0:
|
||||||
miller-rabin "^4.0.0"
|
miller-rabin "^4.0.0"
|
||||||
randombytes "^2.0.0"
|
randombytes "^2.0.0"
|
||||||
|
|
||||||
|
dijkstrajs@^1.0.1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
|
||||||
|
integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
|
||||||
|
|
||||||
dir-glob@^3.0.1:
|
dir-glob@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||||
|
@ -3856,6 +3875,11 @@ emojis-list@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||||
|
|
||||||
|
encode-utf8@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
|
||||||
|
integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
|
||||||
|
|
||||||
encodeurl@~1.0.2:
|
encodeurl@~1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||||
|
@ -4389,7 +4413,7 @@ find-up@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
locate-path "^3.0.0"
|
locate-path "^3.0.0"
|
||||||
|
|
||||||
find-up@^4.0.0:
|
find-up@^4.0.0, find-up@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
|
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
|
||||||
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
|
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
|
||||||
|
@ -4599,6 +4623,11 @@ get-caller-file@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
|
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
|
||||||
integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
|
integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
|
||||||
|
|
||||||
|
get-caller-file@^2.0.1:
|
||||||
|
version "2.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||||
|
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||||
|
|
||||||
get-intrinsic@^1.0.2:
|
get-intrinsic@^1.0.2:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
|
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
|
||||||
|
@ -7396,6 +7425,11 @@ plur@^4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
irregular-plurals "^3.2.0"
|
irregular-plurals "^3.2.0"
|
||||||
|
|
||||||
|
pngjs@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
|
||||||
|
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
||||||
|
|
||||||
pnp-webpack-plugin@^1.6.4:
|
pnp-webpack-plugin@^1.6.4:
|
||||||
version "1.6.4"
|
version "1.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
|
resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
|
||||||
|
@ -8294,6 +8328,16 @@ q@^1.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||||
|
|
||||||
|
qrcode@^1.5.0:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b"
|
||||||
|
integrity sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==
|
||||||
|
dependencies:
|
||||||
|
dijkstrajs "^1.0.1"
|
||||||
|
encode-utf8 "^1.0.3"
|
||||||
|
pngjs "^5.0.0"
|
||||||
|
yargs "^15.3.1"
|
||||||
|
|
||||||
qs@6.7.0:
|
qs@6.7.0:
|
||||||
version "6.7.0"
|
version "6.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||||
|
@ -8635,6 +8679,11 @@ require-main-filename@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
|
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
|
||||||
integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
|
integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
|
||||||
|
|
||||||
|
require-main-filename@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||||
|
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||||
|
|
||||||
requires-port@^1.0.0:
|
requires-port@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||||
|
@ -9177,6 +9226,13 @@ spdx-license-ids@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f"
|
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f"
|
||||||
integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==
|
integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==
|
||||||
|
|
||||||
|
speakeasy@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/speakeasy/-/speakeasy-2.0.0.tgz#85c91a071b09a5cb8642590d983566165f57613a"
|
||||||
|
integrity sha1-hckaBxsJpcuGQlkNmDVmFl9XYTo=
|
||||||
|
dependencies:
|
||||||
|
base32.js "0.0.1"
|
||||||
|
|
||||||
split-on-first@^1.0.0:
|
split-on-first@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
|
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
|
||||||
|
@ -10528,7 +10584,7 @@ wrap-ansi@^2.0.0:
|
||||||
string-width "^1.0.1"
|
string-width "^1.0.1"
|
||||||
strip-ansi "^3.0.1"
|
strip-ansi "^3.0.1"
|
||||||
|
|
||||||
wrap-ansi@^6.0.0:
|
wrap-ansi@^6.0.0, wrap-ansi@^6.2.0:
|
||||||
version "6.2.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
||||||
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
||||||
|
@ -10640,6 +10696,14 @@ yargs-parser@^11.1.1:
|
||||||
camelcase "^5.0.0"
|
camelcase "^5.0.0"
|
||||||
decamelize "^1.2.0"
|
decamelize "^1.2.0"
|
||||||
|
|
||||||
|
yargs-parser@^18.1.2:
|
||||||
|
version "18.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
|
||||||
|
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
|
||||||
|
dependencies:
|
||||||
|
camelcase "^5.0.0"
|
||||||
|
decamelize "^1.2.0"
|
||||||
|
|
||||||
yargs@^12.0.1:
|
yargs@^12.0.1:
|
||||||
version "12.0.5"
|
version "12.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
|
||||||
|
@ -10658,6 +10722,23 @@ yargs@^12.0.1:
|
||||||
y18n "^3.2.1 || ^4.0.0"
|
y18n "^3.2.1 || ^4.0.0"
|
||||||
yargs-parser "^11.1.1"
|
yargs-parser "^11.1.1"
|
||||||
|
|
||||||
|
yargs@^15.3.1:
|
||||||
|
version "15.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||||
|
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||||
|
dependencies:
|
||||||
|
cliui "^6.0.0"
|
||||||
|
decamelize "^1.2.0"
|
||||||
|
find-up "^4.1.0"
|
||||||
|
get-caller-file "^2.0.1"
|
||||||
|
require-directory "^2.1.1"
|
||||||
|
require-main-filename "^2.0.0"
|
||||||
|
set-blocking "^2.0.0"
|
||||||
|
string-width "^4.2.0"
|
||||||
|
which-module "^2.0.0"
|
||||||
|
y18n "^4.0.0"
|
||||||
|
yargs-parser "^18.1.2"
|
||||||
|
|
||||||
yauzl@^2.10.0:
|
yauzl@^2.10.0:
|
||||||
version "2.10.0"
|
version "2.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
|
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
|
||||||
|
|
Reference in New Issue