Add MFA support
This commit is contained in:
parent
d68d5141c8
commit
e3ecfb0d37
73
app/assets/app/auth/MFAChallenge.component.js
Normal file
73
app/assets/app/auth/MFAChallenge.component.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { Component } from '../../lib/vues6/vues6.js'
|
||||||
|
import { auth_api } from '../service/AuthApi.service.js'
|
||||||
|
import { location_service } from '../service/Location.service.js'
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div class="coreid-auth-page col-lg-6 col-md-8 col-sm-10 col-xs-12 offset-lg-3 offset-md-2 offset-sm-1 offset-xs-0 text-left">
|
||||||
|
<div class="coreid-auth-page-inner">
|
||||||
|
<div class="coreid-header font-weight-light">{{ app_name }}</div>
|
||||||
|
<div class="coreid-message">
|
||||||
|
Your account has multi-factor authentication enabled. Please enter the code generated by your authenticator app to continue.
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="number"
|
||||||
|
placeholder="2FA Code"
|
||||||
|
v-model="verify_code"
|
||||||
|
@keyup="on_key_up"
|
||||||
|
name="verify_code"
|
||||||
|
maxlength="6"
|
||||||
|
:disabled="verify_success"
|
||||||
|
ref="verify_input"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-if="error_message" class="error-message">{{ error_message }}</div>
|
||||||
|
<div v-if="other_message" class="other-message">{{ other_message }}</div>
|
||||||
|
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
export default class MFAChallengePage extends Component {
|
||||||
|
static get selector() { return 'coreid-mfa-challenge-page' }
|
||||||
|
static get props() { return ['app_name'] }
|
||||||
|
static get template() { return template }
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
|
||||||
|
verify_code = ''
|
||||||
|
verify_success = false
|
||||||
|
|
||||||
|
error_message = ''
|
||||||
|
other_message = ''
|
||||||
|
|
||||||
|
async watch_verify_code(new_verify_code, old_verify_code) {
|
||||||
|
if ( new_verify_code.length === 6 ) {
|
||||||
|
this.loading = true
|
||||||
|
const result = await auth_api.mfa_attempt(new_verify_code)
|
||||||
|
this.verify_success = result && result.is_valid
|
||||||
|
if ( !this.verify_success ) {
|
||||||
|
this.other_message = ''
|
||||||
|
this.error_message = `Uh, oh! It looks like that's not the right code. Please try again.`
|
||||||
|
this.loading = false
|
||||||
|
} else {
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = `Success! Redirecting...`
|
||||||
|
await location_service.redirect(result.next_destination, 1500)
|
||||||
|
}
|
||||||
|
} else if ( new_verify_code.length > 6 ) {
|
||||||
|
this.verify_code = old_verify_code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_key_up(event) {
|
||||||
|
if ( event.keyCode === 13 ) {
|
||||||
|
// Enter was pressed - don't submit the form
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
148
app/assets/app/auth/MFASetup.component.js
Normal file
148
app/assets/app/auth/MFASetup.component.js
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { Component } from '../../lib/vues6/vues6.js'
|
||||||
|
import { auth_api } from '../service/AuthApi.service.js'
|
||||||
|
import { location_service } from '../service/Location.service.js'
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div class="coreid-auth-page col-lg-6 col-md-8 col-sm-10 col-xs-12 offset-lg-3 offset-md-2 offset-sm-1 offset-xs-0 text-left">
|
||||||
|
<div class="coreid-auth-page-inner">
|
||||||
|
<div class="coreid-header font-weight-light">{{ app_name }}</div>
|
||||||
|
<span v-if="step === 0">
|
||||||
|
<div class="coreid-message">
|
||||||
|
We're going to walk you through setting up multi-factor authentication for your account.
|
||||||
|
<br><br>
|
||||||
|
Once this is completed, you will need to provide your second factor of authentication whenever you sign in with your {{ app_name }} account.
|
||||||
|
<br><br>
|
||||||
|
You'll need some kind of MFA token generator such as Google Authenticator.
|
||||||
|
</div>
|
||||||
|
<div class="buttons text-right pad-top">
|
||||||
|
<button type="button" class="btn btn-primary" @click="back_click">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="continue_click">Continue</button>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span v-if="step === 1">
|
||||||
|
<div class="coreid-message">
|
||||||
|
Scan the QR code below with your authenticator app to add your {{ app_name }} account.
|
||||||
|
<br><br>
|
||||||
|
Once you've done this, we'll ask for one of the generated codes to verify that MFA is working correctly.
|
||||||
|
</div>
|
||||||
|
<div class="coreid-auth-image text-center">
|
||||||
|
<img class="img-fluid" :src="qr_data">
|
||||||
|
<br><br><small>Secret: {{ secret }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="buttons text-right pad-top">
|
||||||
|
<button class="btn btn-primary" type="button" @click="continue_click">Continue</button>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span v-if="step === 2">
|
||||||
|
<div class="coreid-message">
|
||||||
|
Now, enter the code displayed in your authenticator app. {{ app_name }} will verify that the code is
|
||||||
|
correct. Then, you can enable MFA for your account.
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="number"
|
||||||
|
placeholder="2FA Code"
|
||||||
|
v-model="verify_code"
|
||||||
|
@keyup="on_key_up"
|
||||||
|
name="verify_code"
|
||||||
|
maxlength="6"
|
||||||
|
:disabled="verify_success"
|
||||||
|
ref="verify_input"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-if="error_message" class="error-message">{{ error_message }}</div>
|
||||||
|
<div v-if="other_message" class="other-message">{{ other_message }}</div>
|
||||||
|
<div class="buttons text-right pad-top">
|
||||||
|
<button class="btn btn-primary" type="button" @click="back_click">Back</button>
|
||||||
|
<button class="btn btn-primary" type="button" @click="continue_click" :disabled="!verify_success">Enable 2FA</button>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
export default class MFASetupPage extends Component {
|
||||||
|
static get selector() { return 'coreid-mfa-setup-page' }
|
||||||
|
static get props() { return ['app_name'] }
|
||||||
|
static get template() { return template }
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
step = 0
|
||||||
|
|
||||||
|
qr_data = ''
|
||||||
|
otpauth_url = ''
|
||||||
|
secret = ''
|
||||||
|
verify_code = ''
|
||||||
|
|
||||||
|
verify_success = false
|
||||||
|
|
||||||
|
error_message = ''
|
||||||
|
other_message = ''
|
||||||
|
|
||||||
|
async watch_verify_code(new_verify_code, old_verify_code) {
|
||||||
|
if ( new_verify_code.length === 6 ) {
|
||||||
|
this.loading = true
|
||||||
|
const result = await auth_api.mfa_attempt(new_verify_code)
|
||||||
|
this.verify_success = result && result.is_valid
|
||||||
|
this.loading = false
|
||||||
|
if ( !this.verify_success ) {
|
||||||
|
this.other_message = ''
|
||||||
|
this.error_message = `Uh, oh! It looks like that's not the right code. Please try again.`
|
||||||
|
} else {
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = `Success! That code matched what ${this.app_name} was expecting. You can now enable multi-factor authentication for your account.`
|
||||||
|
}
|
||||||
|
} else if ( new_verify_code.length > 6 ) {
|
||||||
|
this.verify_code = old_verify_code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async back_click() {
|
||||||
|
if ( this.step === 0 ) await location_service.back()
|
||||||
|
else this.step -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_key_up(event) {
|
||||||
|
if ( event.keyCode === 13 ) {
|
||||||
|
// Enter was pressed - don't submit the form
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async continue_click() {
|
||||||
|
if ( this.step === 0 ) {
|
||||||
|
// Get the MFA token
|
||||||
|
// TODO try/catch this
|
||||||
|
this.loading = true
|
||||||
|
const result = await auth_api.mfa_generate()
|
||||||
|
this.qr_data = result.qr_code
|
||||||
|
this.otpauth_url = result.otpauth_url
|
||||||
|
this.secret = result.secret
|
||||||
|
this.step = 1
|
||||||
|
this.loading = false
|
||||||
|
} else if ( this.step === 1 ) {
|
||||||
|
this.step = 2
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = ''
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.verify_input.focus()
|
||||||
|
})
|
||||||
|
} else if ( this.step === 2 ) {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
await auth_api.mfa_enable()
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = 'MFA has been enabled for your account! For security purposes, you will be asked to sign in again.'
|
||||||
|
await location_service.redirect('/auth/login', 3000)
|
||||||
|
} catch(e) {
|
||||||
|
this.loading = false
|
||||||
|
this.error_message = 'Sorry, an unknown error occurred, and we were unable to continue.'
|
||||||
|
this.other_message = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
app/assets/app/auth/Page.component.js
Normal file
35
app/assets/app/auth/Page.component.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Component } from '../../lib/vues6/vues6.js'
|
||||||
|
import { auth_api } from '../service/AuthApi.service.js'
|
||||||
|
import { action_service } from '../service/Action.service.js'
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div class="coreid-auth-page col-lg-6 col-md-8 col-sm-10 col-xs-12 offset-lg-3 offset-md-2 offset-sm-1 offset-xs-0 text-left">
|
||||||
|
<div class="coreid-auth-page-inner">
|
||||||
|
<div class="coreid-header font-weight-light">{{ app_name }}</div>
|
||||||
|
<div class="coreid-message">{{ message }}</div>
|
||||||
|
<div class="buttons text-right pad-top">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index"
|
||||||
|
@click="action_click(index)"
|
||||||
|
>{{ action.text }}</button>
|
||||||
|
</div>
|
||||||
|
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
export default class AuthPage extends Component {
|
||||||
|
static get selector() { return 'coreid-auth-page' }
|
||||||
|
static get props() { return ['app_name', 'message', 'actions'] }
|
||||||
|
static get template() { return template }
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
|
||||||
|
async action_click(index) {
|
||||||
|
this.loading = true
|
||||||
|
await action_service.perform(this.actions[index])
|
||||||
|
}
|
||||||
|
}
|
@ -6,43 +6,43 @@ const template = `
|
|||||||
<div class="coreid-login-form col-lg-6 col-md-8 col-sm-10 col-xs-12 offset-lg-3 offset-md-2 offset-sm-1 offset-xs-0 text-left">
|
<div class="coreid-login-form col-lg-6 col-md-8 col-sm-10 col-xs-12 offset-lg-3 offset-md-2 offset-sm-1 offset-xs-0 text-left">
|
||||||
<div class="coreid-login-form-inner">
|
<div class="coreid-login-form-inner">
|
||||||
<div class="coreid-login-form-header font-weight-light">{{ app_name }}</div>
|
<div class="coreid-login-form-header font-weight-light">{{ app_name }}</div>
|
||||||
<div class="coreid-login-form-message">{{ login_message }}</div>
|
<div class="coreid-login-form-message">{{ login_message }}</div>
|
||||||
<form class="coreid-form" v-on:submit.prevent="do_nothing">
|
<form class="coreid-form" v-on:submit.prevent="do_nothing">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="coreid-login-form-username"
|
id="coreid-login-form-username"
|
||||||
name="username"
|
name="username"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
v-model="username"
|
v-model="username"
|
||||||
autofocus
|
autofocus
|
||||||
@keyup="on_key_up"
|
@keyup="on_key_up"
|
||||||
:disabled="loading || step_two"
|
:disabled="loading || step_two"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" v-if="step_two">
|
<div class="form-group" v-if="step_two">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="coreid-login-form-password"
|
id="coreid-login-form-password"
|
||||||
name="password"
|
name="password"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@keyup="on_key_up"
|
@keyup="on_key_up"
|
||||||
ref="password_input"
|
ref="password_input"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="error_message" class="error-message">{{ error_message }}</div>
|
<div v-if="error_message" class="error-message">{{ error_message }}</div>
|
||||||
<div v-if="other_message" class="other-message">{{ other_message }}</div>
|
<div v-if="other_message" class="other-message">{{ other_message }}</div>
|
||||||
<div class="buttons text-right">
|
<div class="buttons text-right">
|
||||||
<button type="button" class="btn btn-primary" :disabled="loading" v-if="step_two" v-on:click="back_click">Back</button>
|
<button type="button" class="btn btn-primary" :disabled="loading" v-if="step_two" v-on:click="back_click">Back</button>
|
||||||
<button type="button" class="btn btn-primary" :disabled="loading || btn_disabled" v-on:click="step_click">{{ button_text }}</button>
|
<button type="button" class="btn btn-primary" :disabled="loading || btn_disabled" v-on:click="step_click">{{ button_text }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import AuthLoginForm from "./auth/login/Form.component.js"
|
import AuthLoginForm from "./auth/login/Form.component.js"
|
||||||
|
import AuthPage from './auth/Page.component.js'
|
||||||
|
import MFASetupPage from './auth/MFASetup.component.js'
|
||||||
|
import MFAChallengePage from './auth/MFAChallenge.component.js'
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
AuthLoginForm
|
AuthLoginForm,
|
||||||
|
AuthPage,
|
||||||
|
MFASetupPage,
|
||||||
|
MFAChallengePage,
|
||||||
}
|
}
|
||||||
|
|
||||||
export { components }
|
export { components }
|
||||||
|
16
app/assets/app/service/Action.service.js
Normal file
16
app/assets/app/service/Action.service.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { location_service } from './Location.service.js'
|
||||||
|
|
||||||
|
class ActionService {
|
||||||
|
async perform({ text, action, ...args }) {
|
||||||
|
if ( action === 'redirect' ) {
|
||||||
|
if ( args.next ) {
|
||||||
|
return location_service.redirect(args.next, 1500)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new TypeError(`Unknown action type: ${action}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const action_service = new ActionService()
|
||||||
|
export { action_service }
|
@ -17,6 +17,21 @@ class AuthAPI {
|
|||||||
|
|
||||||
return { success: false }
|
return { success: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async mfa_generate() {
|
||||||
|
const result = await axios.post('/api/v1/auth/mfa/generate')
|
||||||
|
return result && result.data && result.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async mfa_attempt(verify_code) {
|
||||||
|
const result = await axios.post('/api/v1/auth/mfa/attempt', { verify_code })
|
||||||
|
return result && result.data && result.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async mfa_enable() {
|
||||||
|
const result = await axios.post('/api/v1/auth/mfa/enable')
|
||||||
|
return result && result.data && result.data.data && result.data.data.success && result.data.data.mfa_enabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auth_api = new AuthAPI()
|
const auth_api = new AuthAPI()
|
||||||
|
@ -7,6 +7,10 @@ class LocationService {
|
|||||||
}, delay)
|
}, delay)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async back() {
|
||||||
|
return window.history.back()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const location_service = new LocationService()
|
const location_service = new LocationService()
|
||||||
|
@ -109,22 +109,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.coreid-login-form {
|
.coreid-login-form, .coreid-auth-page {
|
||||||
border: 2px solid #ddd;
|
border: 2px solid #ddd;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 65vh;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
|
||||||
.coreid-login-form-inner {
|
.coreid-login-form-inner, .coreid-auth-page-inner {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
padding-top: 170px;
|
width: 100%;
|
||||||
padding-bottom: 160px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.coreid-login-form-header {
|
.coreid-login-form-header, .coreid-header {
|
||||||
font-size: 2.5em;
|
font-size: 2.5em;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coreid-login-form-message {
|
.coreid-login-form-message, .coreid-message {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,6 +135,14 @@
|
|||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
&.pad-top {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
background: #666;
|
background: #666;
|
||||||
border-color: #444;
|
border-color: #444;
|
||||||
@ -149,13 +160,13 @@
|
|||||||
|
|
||||||
.coreid-loading-spinner {
|
.coreid-loading-spinner {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #ddd;
|
background-color: rgba(0,0,0,0);
|
||||||
height: 7px;
|
height: 7px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: calc(100% + 30px);
|
width: calc(100% + 30px);
|
||||||
margin-left: -15px;
|
margin-left: -15px;
|
||||||
border-radius: 0 0 5px 5px;
|
margin-top: 70px;
|
||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
height: 7px;
|
height: 7px;
|
||||||
@ -163,7 +174,6 @@
|
|||||||
background-color: #bbb;
|
background-color: #bbb;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
border-radius: 0 0 5px 5px;
|
|
||||||
animation-name: loading-bar;
|
animation-name: loading-bar;
|
||||||
animation-duration: 1.5s;
|
animation-duration: 1.5s;
|
||||||
animation-fill-mode: both;
|
animation-fill-mode: both;
|
||||||
|
@ -28,9 +28,11 @@ class Home extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async tmpl(req, res) {
|
async tmpl(req, res) {
|
||||||
return res.page('tmpl', this.Vue.data({
|
return this.Vue.auth_message(res, {
|
||||||
login_message: 'Please sign-in to continue.'
|
message: 'This is a test message. Hello, baby girl; I love you very very much!.',
|
||||||
}))
|
next_destination: '/auth/login',
|
||||||
|
button_text: 'Continue',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ const { Controller } = require('libflitter')
|
|||||||
|
|
||||||
class AuthController extends Controller {
|
class AuthController extends Controller {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'models', 'auth']
|
return [...super.services, 'models', 'auth', 'MFA', 'output']
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate_username(req, res, next) {
|
async validate_username(req, res, next) {
|
||||||
@ -50,9 +50,14 @@ class AuthController extends Controller {
|
|||||||
await flitter.session(req, user)
|
await flitter.session(req, user)
|
||||||
|
|
||||||
let destination = this.configs.get('auth.default_login_route')
|
let destination = this.configs.get('auth.default_login_route')
|
||||||
if ( req?.session?.auth?.flow ) {
|
if ( req.session.auth.flow ) {
|
||||||
destination = req.session.auth.flow
|
destination = req.session.auth.flow
|
||||||
req.session.auth.flow = false
|
}
|
||||||
|
|
||||||
|
// TODO remember-device feature
|
||||||
|
if ( user.mfa_enabled && !req.session.mfa_remember ) {
|
||||||
|
req.session.auth.in_dmz = true
|
||||||
|
destination = '/auth/mfa/challenge'
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.api({
|
return res.api({
|
||||||
@ -62,6 +67,72 @@ class AuthController extends Controller {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generate_mfa_key(req, res, next) {
|
||||||
|
if ( req.user.mfa_enabled )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`MFA already configured for user. Cannot fetch key.`)
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const MFAToken = this.models.get('auth:MFAToken')
|
||||||
|
const secret = await this.MFA.secret(req.user)
|
||||||
|
|
||||||
|
req.user.mfa_token = new MFAToken({
|
||||||
|
secret: secret.base32,
|
||||||
|
otpauth_url: secret.otpauth_url
|
||||||
|
}, req.user)
|
||||||
|
await req.user.save()
|
||||||
|
|
||||||
|
return res.api({
|
||||||
|
success: true,
|
||||||
|
secret: secret.base32,
|
||||||
|
otpauth_url: secret.otpauth_url,
|
||||||
|
qr_code: await this.MFA.qr_code(secret)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async attempt_mfa(req, res, next) {
|
||||||
|
if ( !req.user.mfa_token )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`The user does not have MFA configured.`)
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const code = req.body.verify_code
|
||||||
|
const token = req.user.mfa_token
|
||||||
|
const is_valid = token.verify(code)
|
||||||
|
|
||||||
|
let next_destination = undefined
|
||||||
|
if ( is_valid ) {
|
||||||
|
req.session.auth.in_dmz = false
|
||||||
|
next_destination = req.session.auth.flow || this.configs.get('auth.default_login_route')
|
||||||
|
delete req.session.auth.flow
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session.mfa_remember = true
|
||||||
|
|
||||||
|
return res.api({
|
||||||
|
success: true,
|
||||||
|
verify_code: code,
|
||||||
|
is_valid,
|
||||||
|
next_destination,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async enable_mfa(req, res, next) {
|
||||||
|
if ( !req.user.mfa_token )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`The user does not have an MFA token configured.`)
|
||||||
|
.api()
|
||||||
|
|
||||||
|
req.user.mfa_enabled = true
|
||||||
|
req.user.save()
|
||||||
|
|
||||||
|
// TODO invalidate existing tokens and other logins
|
||||||
|
const flitter = await this.auth.get_provider('flitter')
|
||||||
|
await flitter.logout(req)
|
||||||
|
|
||||||
|
return res.api({success: true, mfa_enabled: req.user.mfa_enabled})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = AuthController
|
module.exports = exports = AuthController
|
||||||
|
@ -17,6 +17,13 @@ class Forms extends FormController {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async logout_provider_present_success(req, res, next) {
|
||||||
|
return this.Vue.auth_message(res, {
|
||||||
|
message: 'You have been successfully logged out.',
|
||||||
|
next_destination: '/',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = Forms
|
module.exports = exports = Forms
|
||||||
|
44
app/controllers/auth/MFA.controller.js
Normal file
44
app/controllers/auth/MFA.controller.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
const { Controller } = require('libflitter')
|
||||||
|
|
||||||
|
class MFAController extends Controller {
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'Vue', 'configs']
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup(req, res, next) {
|
||||||
|
if ( req.user.mfa_enabled ) {
|
||||||
|
// Already set up!
|
||||||
|
return this.Vue.auth_message(res, {
|
||||||
|
message: 'It looks like your account is already set up for multi-factor authentication. Unable to continue with MFA setup.',
|
||||||
|
next_destination: '/', // TODO update this
|
||||||
|
button_text: 'Okay',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the token setup page
|
||||||
|
return res.page('auth:mfa:setup', {
|
||||||
|
...this.Vue.data()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async challenge(req, res, next) {
|
||||||
|
if ( !req.user.mfa_enabled ) {
|
||||||
|
return this.Vue.auth_message(res, {
|
||||||
|
message: 'Your account is not configured to use multi-factor authentication. Would you like to configure it now?',
|
||||||
|
next_destination: '/auth/mfa/setup',
|
||||||
|
button_text: 'Setup MFA',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !req.session.auth.in_dmz ) {
|
||||||
|
return res.redirect(req.session.auth.flow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the MFA challenge page
|
||||||
|
return res.page('auth:mfa:challenge', {
|
||||||
|
...this.Vue.data()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = MFAController
|
25
app/models/auth/MFAToken.model.js
Normal file
25
app/models/auth/MFAToken.model.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const { Model } = require('flitter-orm')
|
||||||
|
const speakeasy = require('speakeasy')
|
||||||
|
|
||||||
|
class MFATokenModel extends Model {
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'MFA']
|
||||||
|
}
|
||||||
|
|
||||||
|
static get schema() {
|
||||||
|
return {
|
||||||
|
secret: String,
|
||||||
|
otpauth_url: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(value) {
|
||||||
|
return speakeasy.totp.verify({
|
||||||
|
secret: this.secret,
|
||||||
|
encoding: 'base32',
|
||||||
|
token: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = MFATokenModel
|
@ -2,6 +2,7 @@ const AuthUser = require('flitter-auth/model/User')
|
|||||||
const LDAP = require('ldapjs')
|
const LDAP = require('ldapjs')
|
||||||
|
|
||||||
const ActiveScope = require('../scopes/ActiveScope')
|
const ActiveScope = require('../scopes/ActiveScope')
|
||||||
|
const MFAToken = require('./MFAToken.model')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Auth user model. This inherits fields and methods from the default
|
* Auth user model. This inherits fields and methods from the default
|
||||||
@ -21,6 +22,8 @@ class User extends AuthUser {
|
|||||||
email: String,
|
email: String,
|
||||||
ldap_visible: {type: Boolean, default: true},
|
ldap_visible: {type: Boolean, default: true},
|
||||||
active: {type: Boolean, default: true},
|
active: {type: Boolean, default: true},
|
||||||
|
mfa_token: MFAToken,
|
||||||
|
mfa_enabled: {type: Boolean, default: false},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
app/routing/middleware/auth/DMZOnly.middleware.js
Normal file
17
app/routing/middleware/auth/DMZOnly.middleware.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const Middleware = require('libflitter/middleware/Middleware')
|
||||||
|
class DMZOnly extends Middleware {
|
||||||
|
|
||||||
|
async test(req, res, next, args = {}){
|
||||||
|
|
||||||
|
if ( req.is_auth ) return next()
|
||||||
|
else {
|
||||||
|
// If not signed in, save the target url so we can redirect back here after auth
|
||||||
|
req.session.auth.flow = req.originalUrl
|
||||||
|
return res.redirect('/auth/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DMZOnly
|
@ -7,8 +7,25 @@
|
|||||||
*/
|
*/
|
||||||
const Middleware = require('flitter-auth/middleware/UserOnly')
|
const Middleware = require('flitter-auth/middleware/UserOnly')
|
||||||
class UserOnly extends Middleware {
|
class UserOnly extends Middleware {
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'output']
|
||||||
|
}
|
||||||
|
|
||||||
|
async test(req, res, next, args = {}){
|
||||||
|
|
||||||
|
if ( req.is_auth && !req.session.auth.in_dmz ) return next()
|
||||||
|
else if ( req.is_auth ) { // Need an MFA challenge
|
||||||
|
if ( !req.session.auth.flow ) req.session.auth.flow = req.originalUrl
|
||||||
|
return res.redirect('/auth/mfa/challenge')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If not signed in, save the target url so we can redirect back here after auth
|
||||||
|
req.session.auth.flow = req.originalUrl
|
||||||
|
this.output.debug('Set auth flow: '+req.originalUrl)
|
||||||
|
return res.redirect('/auth/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ const auth_routes = {
|
|||||||
post: {
|
post: {
|
||||||
'/validate/username': ['controller::api:v1:Auth.validate_username'],
|
'/validate/username': ['controller::api:v1:Auth.validate_username'],
|
||||||
'/attempt': [ 'controller::api:v1:Auth.attempt' ],
|
'/attempt': [ 'controller::api:v1:Auth.attempt' ],
|
||||||
|
'/mfa/generate': ['middleware::auth:UserOnly', 'controller::api:v1:Auth.generate_mfa_key'],
|
||||||
|
'/mfa/attempt': ['middleware::auth:DMZOnly', 'controller::api:v1:Auth.attempt_mfa'],
|
||||||
|
'/mfa/enable': ['middleware::auth:UserOnly', 'controller::api:v1:Auth.enable_mfa'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
* The general structure is as follows:
|
* The general structure is as follows:
|
||||||
*
|
*
|
||||||
* /auth/{provider name}/{action}
|
* /auth/{provider name}/{action}
|
||||||
|
*
|
||||||
* Individual providers may be interacted with individually, therefore:
|
* Individual providers may be interacted with individually, therefore:
|
||||||
*
|
*
|
||||||
* /auth/flitter/register
|
* /auth/flitter/register
|
||||||
@ -49,7 +49,7 @@ const index = {
|
|||||||
|
|
||||||
'/:provider/logout': [
|
'/:provider/logout': [
|
||||||
'middleware::auth:ProviderRoute',
|
'middleware::auth:ProviderRoute',
|
||||||
'middleware::auth:UserOnly',
|
'middleware::auth:DMZOnly',
|
||||||
'controller::auth:Forms.logout_provider_clean_session',
|
'controller::auth:Forms.logout_provider_clean_session',
|
||||||
|
|
||||||
// Note, this separation is between when the auth action has happened properly
|
// Note, this separation is between when the auth action has happened properly
|
||||||
@ -60,7 +60,7 @@ const index = {
|
|||||||
],
|
],
|
||||||
'/logout': [
|
'/logout': [
|
||||||
'middleware::auth:ProviderRoute',
|
'middleware::auth:ProviderRoute',
|
||||||
'middleware::auth:UserOnly',
|
'middleware::auth:DMZOnly',
|
||||||
'controller::auth:Forms.logout_provider_clean_session',
|
'controller::auth:Forms.logout_provider_clean_session',
|
||||||
'controller::auth:Forms.logout_provider_present_success',
|
'controller::auth:Forms.logout_provider_present_success',
|
||||||
],
|
],
|
||||||
@ -94,16 +94,15 @@ const index = {
|
|||||||
'controller::auth:Forms.login_provider_authenticate_user',
|
'controller::auth:Forms.login_provider_authenticate_user',
|
||||||
'controller::auth:Forms.login_provider_present_success',
|
'controller::auth:Forms.login_provider_present_success',
|
||||||
],
|
],
|
||||||
|
|
||||||
'/:provider/logout': [
|
'/:provider/logout': [
|
||||||
'middleware::auth:ProviderRoute',
|
'middleware::auth:ProviderRoute',
|
||||||
'middleware::auth:UserOnly',
|
'middleware::auth:DMZOnly',
|
||||||
'controller::auth:Forms.logout_provider_clean_session',
|
'controller::auth:Forms.logout_provider_clean_session',
|
||||||
'controller::auth:Forms.logout_provider_present_success',
|
'controller::auth:Forms.logout_provider_present_success',
|
||||||
],
|
],
|
||||||
'/logout': [
|
'/logout': [
|
||||||
'middleware::auth:ProviderRoute',
|
'middleware::auth:ProviderRoute',
|
||||||
'middleware::auth:UserOnly',
|
'middleware::auth:DMZOnly',
|
||||||
'controller::auth:Forms.logout_provider_clean_session',
|
'controller::auth:Forms.logout_provider_clean_session',
|
||||||
'controller::auth:Forms.logout_provider_present_success',
|
'controller::auth:Forms.logout_provider_present_success',
|
||||||
],
|
],
|
||||||
|
24
app/routing/routers/auth/mfa.routes.js
Normal file
24
app/routing/routers/auth/mfa.routes.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const mfa_routes = {
|
||||||
|
prefix: '/auth/mfa',
|
||||||
|
|
||||||
|
middleware: [
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
get: {
|
||||||
|
'/setup': [
|
||||||
|
'middleware::auth:UserOnly',
|
||||||
|
'controller::auth:MFA.setup',
|
||||||
|
],
|
||||||
|
'/challenge': [
|
||||||
|
'middleware::auth:DMZOnly',
|
||||||
|
'controller::auth:MFA.challenge',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
post: {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = mfa_routes
|
27
app/services/MFA.service.js
Normal file
27
app/services/MFA.service.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const { Service } = require('flitter-di')
|
||||||
|
const speakeasy = require('speakeasy')
|
||||||
|
const qrcode = require('qrcode')
|
||||||
|
|
||||||
|
class MFAService extends Service {
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'configs']
|
||||||
|
}
|
||||||
|
|
||||||
|
secret(user) {
|
||||||
|
return speakeasy.generateSecret({
|
||||||
|
length: this.configs.get('auth.mfa.secret_length') ?? 20,
|
||||||
|
name: `${this.configs.get('app.name')} (${user.uid})`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async qr_code(secret) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
qrcode.toDataURL(secret.otpauth_url, (err, image_data) => {
|
||||||
|
if ( err ) reject(err)
|
||||||
|
else resolve(image_data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = MFAService
|
@ -14,6 +14,22 @@ class VueService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth_message(res, {message, next_destination, ...args}) {
|
||||||
|
const text = args.button_text || 'Continue'
|
||||||
|
return res.page('public:message', {
|
||||||
|
...this.data({
|
||||||
|
message,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text,
|
||||||
|
action: 'redirect',
|
||||||
|
next: next_destination,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = VueService
|
module.exports = exports = VueService
|
||||||
|
7
app/views/auth/mfa/challenge.pug
Normal file
7
app/views/auth/mfa/challenge.pug
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
extends ../../theme/public/base
|
||||||
|
|
||||||
|
block append style
|
||||||
|
link(rel='stylesheet' href='/style-asset/form.css')
|
||||||
|
|
||||||
|
block vue
|
||||||
|
coreid-mfa-challenge-page(v-bind:app_name="app_name")
|
7
app/views/auth/mfa/setup.pug
Normal file
7
app/views/auth/mfa/setup.pug
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
extends ../../theme/public/base
|
||||||
|
|
||||||
|
block append style
|
||||||
|
link(rel='stylesheet' href='/style-asset/form.css')
|
||||||
|
|
||||||
|
block vue
|
||||||
|
coreid-mfa-setup-page(v-bind:app_name="app_name")
|
7
app/views/public/message.pug
Normal file
7
app/views/public/message.pug
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
extends ../theme/public/base
|
||||||
|
|
||||||
|
block append style
|
||||||
|
link(rel='stylesheet' href='/style-asset/form.css')
|
||||||
|
|
||||||
|
block vue
|
||||||
|
coreid-auth-page(v-bind:app_name="app_name" v-bind:message="message" v-bind:actions="actions")
|
@ -20,4 +20,4 @@ html(lang='en')
|
|||||||
script(src='/assets/lib/popper/popper-1.16.0.min.js')
|
script(src='/assets/lib/popper/popper-1.16.0.min.js')
|
||||||
script(src='/assets/lib/bootstrap/bootstrap-4.4.1.min.js')
|
script(src='/assets/lib/bootstrap/bootstrap-4.4.1.min.js')
|
||||||
script(src='/assets/lib/vue/vue-2.6.11.js')
|
script(src='/assets/lib/vue/vue-2.6.11.js')
|
||||||
script(src='/assets/lib/vues6/vues6.js')
|
script(src='/assets/lib/vues6/vues6.js' type='module')
|
||||||
|
@ -4,4 +4,4 @@ block append style
|
|||||||
link(rel='stylesheet' href='/style-asset/form.css')
|
link(rel='stylesheet' href='/style-asset/form.css')
|
||||||
|
|
||||||
block vue
|
block vue
|
||||||
coreid-login-form(v-bind:app_name="app_name" v-bind:login_message="login_message")
|
coreid-auth-page(v-bind:app_name="app_name" v-bind:message="message" v-bind:actions="actions")
|
||||||
|
@ -3,6 +3,10 @@ const auth_config = {
|
|||||||
default_provider: env('AUTH_DEFAULT_PROVIDER', 'flitter'),
|
default_provider: env('AUTH_DEFAULT_PROVIDER', 'flitter'),
|
||||||
default_login_route: '/dash',
|
default_login_route: '/dash',
|
||||||
|
|
||||||
|
mfa: {
|
||||||
|
secret_length: env('MFA_SECRET_LENGTH', 20)
|
||||||
|
},
|
||||||
|
|
||||||
servers: {
|
servers: {
|
||||||
// OAuth2 authorization server
|
// OAuth2 authorization server
|
||||||
oauth2: {
|
oauth2: {
|
||||||
|
@ -23,9 +23,11 @@
|
|||||||
"flitter-flap": "^0.5.2",
|
"flitter-flap": "^0.5.2",
|
||||||
"flitter-forms": "^0.8.1",
|
"flitter-forms": "^0.8.1",
|
||||||
"flitter-less": "^0.5.3",
|
"flitter-less": "^0.5.3",
|
||||||
"flitter-orm": "^0.2.4",
|
"flitter-orm": "^0.2.5",
|
||||||
"flitter-upload": "^0.8.0",
|
"flitter-upload": "^0.8.0",
|
||||||
"ldapjs": "^1.0.2",
|
"ldapjs": "^1.0.2",
|
||||||
"libflitter": "^0.50.0"
|
"libflitter": "^0.50.1",
|
||||||
|
"qrcode": "^1.4.4",
|
||||||
|
"speakeasy": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
162
yarn.lock
162
yarn.lock
@ -192,13 +192,20 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
|
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
|
||||||
integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
|
integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
|
||||||
|
|
||||||
"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0":
|
"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2":
|
||||||
version "1.7.2"
|
version "1.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
|
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
|
||||||
integrity sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw==
|
integrity sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw==
|
||||||
dependencies:
|
dependencies:
|
||||||
type-detect "4.0.8"
|
type-detect "4.0.8"
|
||||||
|
|
||||||
|
"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1":
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40"
|
||||||
|
integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/commons" "^1.7.0"
|
||||||
|
|
||||||
"@sinonjs/formatio@^4.0.1":
|
"@sinonjs/formatio@^4.0.1":
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-4.0.1.tgz#50ac1da0c3eaea117ca258b06f4f88a471668bdb"
|
resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-4.0.1.tgz#50ac1da0c3eaea117ca258b06f4f88a471668bdb"
|
||||||
@ -207,6 +214,14 @@
|
|||||||
"@sinonjs/commons" "^1"
|
"@sinonjs/commons" "^1"
|
||||||
"@sinonjs/samsam" "^4.2.0"
|
"@sinonjs/samsam" "^4.2.0"
|
||||||
|
|
||||||
|
"@sinonjs/formatio@^5.0.1":
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089"
|
||||||
|
integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/commons" "^1"
|
||||||
|
"@sinonjs/samsam" "^5.0.2"
|
||||||
|
|
||||||
"@sinonjs/samsam@^4.2.0", "@sinonjs/samsam@^4.2.2":
|
"@sinonjs/samsam@^4.2.0", "@sinonjs/samsam@^4.2.2":
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-4.2.2.tgz#0f6cb40e467865306d8a20a97543a94005204e23"
|
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-4.2.2.tgz#0f6cb40e467865306d8a20a97543a94005204e23"
|
||||||
@ -216,6 +231,15 @@
|
|||||||
lodash.get "^4.4.2"
|
lodash.get "^4.4.2"
|
||||||
type-detect "^4.0.8"
|
type-detect "^4.0.8"
|
||||||
|
|
||||||
|
"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3":
|
||||||
|
version "5.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.0.3.tgz#86f21bdb3d52480faf0892a480c9906aa5a52938"
|
||||||
|
integrity sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/commons" "^1.6.0"
|
||||||
|
lodash.get "^4.4.2"
|
||||||
|
type-detect "^4.0.8"
|
||||||
|
|
||||||
"@sinonjs/text-encoding@^0.7.1":
|
"@sinonjs/text-encoding@^0.7.1":
|
||||||
version "0.7.1"
|
version "0.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
|
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
|
||||||
@ -696,6 +720,16 @@ 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:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
|
||||||
|
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
|
||||||
|
|
||||||
basic-auth@~0.0.1:
|
basic-auth@~0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-0.0.1.tgz#31ddb65843f6c35c6fea7beb46a987cb8ce18924"
|
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-0.0.1.tgz#31ddb65843f6c35c6fea7beb46a987cb8ce18924"
|
||||||
@ -790,6 +824,37 @@ bson@^1.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.1.tgz#4330f5e99104c4e751e7351859e2d408279f2f13"
|
resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.1.tgz#4330f5e99104c4e751e7351859e2d408279f2f13"
|
||||||
integrity sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==
|
integrity sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==
|
||||||
|
|
||||||
|
buffer-alloc-unsafe@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
|
||||||
|
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
|
||||||
|
|
||||||
|
buffer-alloc@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
|
||||||
|
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
|
||||||
|
dependencies:
|
||||||
|
buffer-alloc-unsafe "^1.1.0"
|
||||||
|
buffer-fill "^1.0.0"
|
||||||
|
|
||||||
|
buffer-fill@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
|
||||||
|
integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
|
||||||
|
|
||||||
|
buffer-from@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||||
|
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
||||||
|
|
||||||
|
buffer@^5.4.3:
|
||||||
|
version "5.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
|
||||||
|
integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
|
||||||
|
dependencies:
|
||||||
|
base64-js "^1.0.2"
|
||||||
|
ieee754 "^1.1.4"
|
||||||
|
|
||||||
bunyan@1.3.3:
|
bunyan@1.3.3:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.3.3.tgz#bf4e301c1f0bf888ec678829531f7b5d212e9e81"
|
resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.3.3.tgz#bf4e301c1f0bf888ec678829531f7b5d212e9e81"
|
||||||
@ -1329,6 +1394,11 @@ diff@^4.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||||
|
|
||||||
|
dijkstrajs@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b"
|
||||||
|
integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs=
|
||||||
|
|
||||||
doctypes@^1.1.0:
|
doctypes@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
|
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
|
||||||
@ -1339,6 +1409,11 @@ dotenv@^6.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
|
||||||
integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==
|
integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==
|
||||||
|
|
||||||
|
dotenv@^8.2.0:
|
||||||
|
version "8.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||||
|
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
||||||
|
|
||||||
dtrace-provider@0.4.0, dtrace-provider@~0.4:
|
dtrace-provider@0.4.0, dtrace-provider@~0.4:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.4.0.tgz#0b67bc1cc77e79bf88b87ad20664f4a753ce3f26"
|
resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.4.0.tgz#0b67bc1cc77e79bf88b87ad20664f4a753ce3f26"
|
||||||
@ -1744,15 +1819,19 @@ flitter-less@^0.5.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
express-less "^0.1.0"
|
express-less "^0.1.0"
|
||||||
|
|
||||||
flitter-orm@^0.2.4:
|
flitter-orm@^0.2.5:
|
||||||
version "0.2.4"
|
version "0.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/flitter-orm/-/flitter-orm-0.2.4.tgz#539f7631fd286955b01ce6034a0bb68142540f5d"
|
resolved "https://registry.yarnpkg.com/flitter-orm/-/flitter-orm-0.2.5.tgz#425bc6f2899e3eb689a4b3398c758470ca2c1f60"
|
||||||
integrity sha512-7yhwwzzBpPIyW4VC9nHY+Pe9pM+EFYwljYKkK1BEMy8XNk6JADhcLiwZGJmxK38vQ8D7SEdFpZiux3fB68uVnQ==
|
integrity sha512-bTFmCItZcLpQPc6m86iQUHa3xtn5/uQ+/Zm/JQ2+Eqjw+7HPu4BtskzdT2xAMaQl0ylmKz3Zj9ZKyi7MtuZXHQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
chai "^4.2.0"
|
||||||
|
dotenv "^8.2.0"
|
||||||
flitter-di "^0.4.0"
|
flitter-di "^0.4.0"
|
||||||
json-stringify-safe "^5.0.1"
|
json-stringify-safe "^5.0.1"
|
||||||
|
mocha "^7.1.0"
|
||||||
mongodb "^3.5.1"
|
mongodb "^3.5.1"
|
||||||
object-hash "^2.0.1"
|
object-hash "^2.0.1"
|
||||||
|
sinon "^9.0.0"
|
||||||
uuid "^3.4.0"
|
uuid "^3.4.0"
|
||||||
|
|
||||||
flitter-upload@^0.8.0:
|
flitter-upload@^0.8.0:
|
||||||
@ -2106,6 +2185,11 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@^0.4.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer ">= 2.1.2 < 3"
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
|
ieee754@^1.1.4:
|
||||||
|
version "1.1.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
||||||
|
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
|
||||||
|
|
||||||
ignore-walk@^3.0.1:
|
ignore-walk@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
|
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
|
||||||
@ -2317,6 +2401,11 @@ isarray@0.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||||
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
|
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
|
||||||
|
|
||||||
|
isarray@^2.0.1:
|
||||||
|
version "2.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||||
|
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
|
||||||
|
|
||||||
isarray@~1.0.0:
|
isarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||||
@ -2603,10 +2692,10 @@ leven@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
|
resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
|
||||||
integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=
|
integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=
|
||||||
|
|
||||||
libflitter@^0.50.0:
|
libflitter@^0.50.1:
|
||||||
version "0.50.0"
|
version "0.50.1"
|
||||||
resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.50.0.tgz#fdf00f13e806559c50c98cbcfd87d18b5119df40"
|
resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.50.1.tgz#afaab6bc4ae9afe1855b2cc00b22544c47a13c85"
|
||||||
integrity sha512-pA8BkvEWdrinAAI4Ef/IOgDajsHyHYPpDVywPc87FerJqIpLXD0vtx1hIE5HAzzK+Tf/n11gceaxBFajVTpkeA==
|
integrity sha512-bHTwhCg5kXDYbXRBaGbiR/4QW35g9YUIho7mX+Yirj1ZJSOcXUDN0mCHd9x4v235ss2Im7QxUxjgTz8zMxba2g==
|
||||||
dependencies:
|
dependencies:
|
||||||
colors "^1.3.3"
|
colors "^1.3.3"
|
||||||
connect-mongodb-session "^2.2.0"
|
connect-mongodb-session "^2.2.0"
|
||||||
@ -2618,7 +2707,7 @@ libflitter@^0.50.0:
|
|||||||
express-graphql "^0.9.0"
|
express-graphql "^0.9.0"
|
||||||
express-session "^1.15.6"
|
express-session "^1.15.6"
|
||||||
flitter-di "^0.5.0"
|
flitter-di "^0.5.0"
|
||||||
flitter-orm "^0.2.4"
|
flitter-orm "^0.2.5"
|
||||||
graphql "^14.5.4"
|
graphql "^14.5.4"
|
||||||
http-status "^1.4.2"
|
http-status "^1.4.2"
|
||||||
pug "^2.0.3"
|
pug "^2.0.3"
|
||||||
@ -2865,7 +2954,7 @@ mkdirp@^1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea"
|
||||||
integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==
|
integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==
|
||||||
|
|
||||||
mocha@^7.0.1:
|
mocha@^7.0.1, mocha@^7.1.0:
|
||||||
version "7.1.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.1.1.tgz#89fbb30d09429845b1bb893a830bf5771049a441"
|
resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.1.1.tgz#89fbb30d09429845b1bb893a830bf5771049a441"
|
||||||
integrity sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==
|
integrity sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==
|
||||||
@ -3020,6 +3109,17 @@ nise@^3.0.1:
|
|||||||
lolex "^5.0.1"
|
lolex "^5.0.1"
|
||||||
path-to-regexp "^1.7.0"
|
path-to-regexp "^1.7.0"
|
||||||
|
|
||||||
|
nise@^4.0.1:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.3.tgz#9f79ff02fa002ed5ffbc538ad58518fa011dc913"
|
||||||
|
integrity sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/commons" "^1.7.0"
|
||||||
|
"@sinonjs/fake-timers" "^6.0.0"
|
||||||
|
"@sinonjs/text-encoding" "^0.7.1"
|
||||||
|
just-extend "^4.0.2"
|
||||||
|
path-to-regexp "^1.7.0"
|
||||||
|
|
||||||
node-environment-flags@1.0.6:
|
node-environment-flags@1.0.6:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088"
|
resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088"
|
||||||
@ -3401,6 +3501,11 @@ pkg-dir@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^4.0.0"
|
find-up "^4.0.0"
|
||||||
|
|
||||||
|
pngjs@^3.3.0:
|
||||||
|
version "3.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
|
||||||
|
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
|
||||||
|
|
||||||
precond@0.2:
|
precond@0.2:
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac"
|
resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac"
|
||||||
@ -3573,6 +3678,19 @@ 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.4.4:
|
||||||
|
version "1.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"
|
||||||
|
integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==
|
||||||
|
dependencies:
|
||||||
|
buffer "^5.4.3"
|
||||||
|
buffer-alloc "^1.2.0"
|
||||||
|
buffer-from "^1.1.1"
|
||||||
|
dijkstrajs "^1.0.1"
|
||||||
|
isarray "^2.0.1"
|
||||||
|
pngjs "^3.3.0"
|
||||||
|
yargs "^13.2.4"
|
||||||
|
|
||||||
qs@6.5.2, qs@~6.5.2:
|
qs@6.5.2, qs@~6.5.2:
|
||||||
version "6.5.2"
|
version "6.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||||
@ -4027,6 +4145,19 @@ sinon@^8.1.1:
|
|||||||
nise "^3.0.1"
|
nise "^3.0.1"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
|
sinon@^9.0.0:
|
||||||
|
version "9.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d"
|
||||||
|
integrity sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/commons" "^1.7.2"
|
||||||
|
"@sinonjs/fake-timers" "^6.0.1"
|
||||||
|
"@sinonjs/formatio" "^5.0.1"
|
||||||
|
"@sinonjs/samsam" "^5.0.3"
|
||||||
|
diff "^4.0.2"
|
||||||
|
nise "^4.0.1"
|
||||||
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
slash@^1.0.0:
|
slash@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
|
||||||
@ -4075,6 +4206,13 @@ spawn-wrap@^2.0.0:
|
|||||||
signal-exit "^3.0.2"
|
signal-exit "^3.0.2"
|
||||||
which "^2.0.1"
|
which "^2.0.1"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
sprintf-js@~1.0.2:
|
sprintf-js@~1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||||
@ -4674,7 +4812,7 @@ yargs-unparser@1.6.0:
|
|||||||
lodash "^4.17.15"
|
lodash "^4.17.15"
|
||||||
yargs "^13.3.0"
|
yargs "^13.3.0"
|
||||||
|
|
||||||
yargs@13.3.2, yargs@^13.3.0:
|
yargs@13.3.2, yargs@^13.2.4, yargs@^13.3.0:
|
||||||
version "13.3.2"
|
version "13.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
|
||||||
integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
|
integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
|
||||||
|
Loading…
Reference in New Issue
Block a user