wip(backend): Added MFA support
This commit is contained in:
parent
36726dbdc3
commit
a5ee0a0854
13 changed files with 380 additions and 14 deletions
|
@ -67,6 +67,14 @@ BACKEND_JWT_ISSUER=freesewing.org
|
||||||
BACKEND_JWT_EXPIRY=7d
|
BACKEND_JWT_EXPIRY=7d
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Multi-Factor Authentication (MFA) #
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
# The service for the token generator (think Google Authenticator)
|
||||||
|
#BACKEND_MFA_SERVICE=FreeSewing
|
||||||
|
|
||||||
|
|
||||||
#####################################################################
|
#####################################################################
|
||||||
# Email (via AWS SES) #
|
# Email (via AWS SES) #
|
||||||
# #
|
# #
|
||||||
|
|
|
@ -29,10 +29,12 @@
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"express": "4.18.2",
|
"express": "4.18.2",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-http": "^0.3.0",
|
"passport-http": "^0.3.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"pino": "^8.7.0"
|
"pino": "^8.7.0",
|
||||||
|
"qrcode": "^1.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai-http": "^4.3.0",
|
"chai-http": "^4.3.0",
|
||||||
|
|
|
@ -55,6 +55,8 @@ model User {
|
||||||
language String @default("en")
|
language String @default("en")
|
||||||
lastLogin DateTime?
|
lastLogin DateTime?
|
||||||
lusername String @unique
|
lusername String @unique
|
||||||
|
mfaSecret String @default("")
|
||||||
|
mfaEnabled Boolean @default(false)
|
||||||
newsletter Boolean @default(false)
|
newsletter Boolean @default(false)
|
||||||
password String
|
password String
|
||||||
patron Int @default(0)
|
patron Int @default(0)
|
||||||
|
|
|
@ -58,6 +58,9 @@ const config = {
|
||||||
},
|
},
|
||||||
languages: ['en', 'de', 'es', 'fr', 'nl'],
|
languages: ['en', 'de', 'es', 'fr', 'nl'],
|
||||||
measies: measurements,
|
measies: measurements,
|
||||||
|
mfa: {
|
||||||
|
service: process.env.BACKEND_MFA_SERVICE || 'FreeSewing',
|
||||||
|
},
|
||||||
port,
|
port,
|
||||||
roles: {
|
roles: {
|
||||||
levels: {
|
levels: {
|
||||||
|
|
|
@ -65,3 +65,16 @@ UsersController.prototype.update = async (req, res, tools) => {
|
||||||
|
|
||||||
return User.sendResponse(res)
|
return User.sendResponse(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Updates the MFA setting of the authenticated user
|
||||||
|
*
|
||||||
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
|
*/
|
||||||
|
UsersController.prototype.updateMfa = async (req, res, tools) => {
|
||||||
|
const User = new UserModel(tools)
|
||||||
|
await User.guardedRead({ id: req.user.uid }, req)
|
||||||
|
await User.guardedMfaUpdate(req)
|
||||||
|
|
||||||
|
return User.sendResponse(res)
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { verifyConfig } from './config.mjs'
|
||||||
import { loadExpressMiddleware, loadPassportMiddleware } from './middleware.mjs'
|
import { loadExpressMiddleware, loadPassportMiddleware } from './middleware.mjs'
|
||||||
// Encryption
|
// Encryption
|
||||||
import { encryption } from './utils/crypto.mjs'
|
import { encryption } from './utils/crypto.mjs'
|
||||||
|
// Multi-Factor Authentication (MFA)
|
||||||
|
import { mfa } from './utils/mfa.mjs'
|
||||||
// Email
|
// Email
|
||||||
import { mailer } from './utils/email.mjs'
|
import { mailer } from './utils/email.mjs'
|
||||||
|
|
||||||
|
@ -28,6 +30,7 @@ const tools = {
|
||||||
passport,
|
passport,
|
||||||
prisma,
|
prisma,
|
||||||
...encryption(config.encryption.key),
|
...encryption(config.encryption.key),
|
||||||
|
...mfa(config.mfa),
|
||||||
...mailer(config),
|
...mailer(config),
|
||||||
config,
|
config,
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,10 @@ export function UserModel(tools) {
|
||||||
this.prisma = tools.prisma
|
this.prisma = tools.prisma
|
||||||
this.decrypt = tools.decrypt
|
this.decrypt = tools.decrypt
|
||||||
this.encrypt = tools.encrypt
|
this.encrypt = tools.encrypt
|
||||||
|
this.mfa = tools.mfa
|
||||||
this.mailer = tools.email
|
this.mailer = tools.email
|
||||||
this.Confirmation = new ConfirmationModel(tools)
|
this.Confirmation = new ConfirmationModel(tools)
|
||||||
this.encryptedFields = ['bio', 'github', 'email', 'initial', 'img']
|
this.encryptedFields = ['bio', 'github', 'email', 'initial', 'img', 'mfaSecret']
|
||||||
this.clear = {} // For holding decrypted data
|
this.clear = {} // For holding decrypted data
|
||||||
|
|
||||||
return this
|
return this
|
||||||
|
@ -163,6 +164,8 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
|
||||||
username,
|
username,
|
||||||
lusername: username,
|
lusername: username,
|
||||||
language: body.language,
|
language: body.language,
|
||||||
|
mfaEnabled: false,
|
||||||
|
mfaSecret: this.encrypt(''),
|
||||||
password: asJson(hashPassword(randomString())), // We'll change this later
|
password: asJson(hashPassword(randomString())), // We'll change this later
|
||||||
github: this.encrypt(''),
|
github: this.encrypt(''),
|
||||||
bio: this.encrypt(''),
|
bio: this.encrypt(''),
|
||||||
|
@ -240,9 +243,16 @@ UserModel.prototype.passwordLogin = async function (req) {
|
||||||
return this.setResponse(401, 'loginFailed')
|
return this.setResponse(401, 'loginFailed')
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Login by user ${this.record.id} (${this.record.username})`)
|
// Check for MFA
|
||||||
|
if (this.record.mfaEnabled) {
|
||||||
|
if (!req.body.token) return this.setResponse(200, false, { note: 'mfaTokenRequired' })
|
||||||
|
else if (!this.mfa.verify(req.body.token, this.clear.mfaSecret)) {
|
||||||
|
return this.setResponse(401, 'loginFailed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Login success
|
// Login success
|
||||||
|
log.info(`Login by user ${this.record.id} (${this.record.username})`)
|
||||||
if (updatedPasswordField) {
|
if (updatedPasswordField) {
|
||||||
// Update the password field with a v3 hash
|
// Update the password field with a v3 hash
|
||||||
await this.unguardedUpdate({ password: updatedPasswordField })
|
await this.unguardedUpdate({ password: updatedPasswordField })
|
||||||
|
@ -420,6 +430,59 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
|
||||||
return this.setResponse(200, false, returnData)
|
return this.setResponse(200, false, returnData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enables/Disables MFA on the account - Used when we pass through
|
||||||
|
* user-provided data so we can't be certain it's safe
|
||||||
|
*/
|
||||||
|
UserModel.prototype.guardedMfaUpdate = async function ({ body, user }) {
|
||||||
|
if (user.level < 4) return this.setResponse(403, 'insufficientAccessLevel')
|
||||||
|
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||||
|
if (body.mfa === true && this.record.mfaEnabled === true)
|
||||||
|
return this.setResponse(400, 'mfaActive')
|
||||||
|
|
||||||
|
// Disable
|
||||||
|
if (body.mfa === false) {
|
||||||
|
}
|
||||||
|
// Confirm
|
||||||
|
else if (body.mfa === true && body.token && body.secret) {
|
||||||
|
if (body.secret === this.clear.mfaSecret && this.mfa.verify(body.token, this.clear.mfaSecret)) {
|
||||||
|
// Looks good. Enable MFA
|
||||||
|
try {
|
||||||
|
await this.unguardedUpdate({
|
||||||
|
mfaEnabled: true,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(err, 'Could not enable MFA after token check')
|
||||||
|
return this.setResponse(500, 'mfaActivationFailed')
|
||||||
|
}
|
||||||
|
return this.setResponse(200, false, {})
|
||||||
|
} else return this.setResponse(403, 'mfaTokenInvalid')
|
||||||
|
}
|
||||||
|
// Enroll
|
||||||
|
else if (body.mfa === true && this.record.mfaEnabled === false) {
|
||||||
|
let mfa
|
||||||
|
try {
|
||||||
|
mfa = await this.mfa.enroll(this.record.username)
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(err, 'Failed to enroll MFA')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update mfaSecret
|
||||||
|
try {
|
||||||
|
await this.unguardedUpdate({
|
||||||
|
mfaSecret: this.encrypt(mfa.secret),
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(err, 'Could not update username after user creation')
|
||||||
|
return this.setResponse(500, 'usernameUpdateAfterUserCreationFailed')
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.setResponse(200, false, { mfa })
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.setResponse(400, 'invalidMfaSetting')
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns account data
|
* Returns account data
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -17,7 +17,6 @@ export function usersRoutes(tools) {
|
||||||
app.post('/login', (req, res) => Users.login(req, res, tools))
|
app.post('/login', (req, res) => Users.login(req, res, tools))
|
||||||
|
|
||||||
// Read current jwt
|
// Read current jwt
|
||||||
|
|
||||||
app.get('/whoami/jwt', passport.authenticate(...jwt), (req, res) => Users.whoami(req, res, tools))
|
app.get('/whoami/jwt', passport.authenticate(...jwt), (req, res) => Users.whoami(req, res, tools))
|
||||||
app.get('/account/jwt', passport.authenticate(...jwt), (req, res) =>
|
app.get('/account/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
Users.whoami(req, res, tools)
|
Users.whoami(req, res, tools)
|
||||||
|
@ -34,6 +33,13 @@ export function usersRoutes(tools) {
|
||||||
Users.update(req, res, tools)
|
Users.update(req, res, tools)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Enable MFA (totp)
|
||||||
|
app.post('/account/mfa/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
|
Users.updateMfa(req, res, tools)
|
||||||
|
)
|
||||||
|
app.post('/account/mfa/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Users.updateMfa(req, res, tools)
|
||||||
|
)
|
||||||
/*
|
/*
|
||||||
|
|
||||||
// Remove account
|
// Remove account
|
||||||
|
|
30
sites/backend/src/utils/mfa.mjs
Normal file
30
sites/backend/src/utils/mfa.mjs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import qrcode from 'qrcode'
|
||||||
|
import { authenticator } from '@otplib/preset-default'
|
||||||
|
|
||||||
|
const dark = '#AAAAAA'
|
||||||
|
const light = '#EEEEEE'
|
||||||
|
/*
|
||||||
|
* Exporting this closure that makes sure we have access to the
|
||||||
|
* instantiated config
|
||||||
|
*/
|
||||||
|
export const mfa = ({ service }) => ({
|
||||||
|
mfa: {
|
||||||
|
enroll: async (user) => {
|
||||||
|
const secret = authenticator.generateSecret()
|
||||||
|
const otpauth = authenticator.keyuri(user, service, secret)
|
||||||
|
let svg
|
||||||
|
try {
|
||||||
|
svg = await qrcode.toString(otpauth, {
|
||||||
|
type: 'svg',
|
||||||
|
color: { dark, light },
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
svg = svg.replace(dark, 'currentColor').replace(light, 'none')
|
||||||
|
|
||||||
|
return { secret, otpauth, qrcode: svg }
|
||||||
|
},
|
||||||
|
verify: (token, secret) => authenticator.check(token, secret),
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,4 +1,5 @@
|
||||||
import { userTests } from './user.mjs'
|
import { userTests } from './user.mjs'
|
||||||
|
import { mfaTests } from './mfa.mjs'
|
||||||
import { accountTests } from './account.mjs'
|
import { accountTests } from './account.mjs'
|
||||||
import { apikeyTests } from './apikey.mjs'
|
import { apikeyTests } from './apikey.mjs'
|
||||||
import { personTests } from './person.mjs'
|
import { personTests } from './person.mjs'
|
||||||
|
@ -7,6 +8,7 @@ import { setup } from './shared.mjs'
|
||||||
|
|
||||||
const runTests = async (...params) => {
|
const runTests = async (...params) => {
|
||||||
await userTests(...params)
|
await userTests(...params)
|
||||||
|
await mfaTests(...params)
|
||||||
await apikeyTests(...params)
|
await apikeyTests(...params)
|
||||||
await accountTests(...params)
|
await accountTests(...params)
|
||||||
await personTests(...params)
|
await personTests(...params)
|
||||||
|
|
156
sites/backend/tests/mfa.mjs
Normal file
156
sites/backend/tests/mfa.mjs
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import { authenticator } from '@otplib/preset-default'
|
||||||
|
|
||||||
|
export const mfaTests = async (chai, config, expect, store) => {
|
||||||
|
const secret = {
|
||||||
|
jwt: store.account,
|
||||||
|
key: store.altaccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auth in secret) {
|
||||||
|
describe(`${store.icon('mfa', auth)} Setup Multi-Factor Authentication (MFA) (${auth})`, () => {
|
||||||
|
it(`${store.icon('mfa')} Should return 400 on MFA enable without proper value`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.post(`/account/mfa/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + secret[auth].token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${secret[auth].apikey.key}:${secret[auth].apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.send({ mfa: 'yes' })
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(400)
|
||||||
|
expect(res.body.result).to.equal(`error`)
|
||||||
|
expect(res.body.error).to.equal(`invalidMfaSetting`)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${store.icon('mfa', auth)} Should return MFA secret and QR code`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.post(`/account/mfa/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + secret[auth].token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${secret[auth].apikey.key}:${secret[auth].apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.send({ mfa: true })
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
expect(typeof res.body.mfa.secret).to.equal(`string`)
|
||||||
|
expect(typeof res.body.mfa.otpauth).to.equal(`string`)
|
||||||
|
expect(typeof res.body.mfa.qrcode).to.equal(`string`)
|
||||||
|
secret[auth].mfaSecret = res.body.mfa.secret
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${store.icon('mfa', auth)} Should enable MFA after validating the token`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.post(`/account/mfa/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + secret[auth].token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${secret[auth].apikey.key}:${secret[auth].apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.send({
|
||||||
|
mfa: true,
|
||||||
|
secret: secret[auth].mfaSecret,
|
||||||
|
token: authenticator.generate(secret[auth].mfaSecret),
|
||||||
|
})
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${store.icon('mfa', auth)} Should not request MFA when it's already active`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.post(`/account/mfa/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + secret[auth].token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${secret[auth].apikey.key}:${secret[auth].apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.send({ mfa: true })
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(400)
|
||||||
|
expect(res.body.result).to.equal(`error`)
|
||||||
|
expect(res.body.error).to.equal(`mfaActive`)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${store.icon('mfa', auth)} Should not enable MFA when it's already active`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.post(`/account/mfa/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + secret[auth].token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${secret[auth].apikey.key}:${secret[auth].apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.send({
|
||||||
|
mfa: true,
|
||||||
|
secret: secret[auth].mfaSecret,
|
||||||
|
token: authenticator.generate(secret[auth].mfaSecret),
|
||||||
|
})
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(400)
|
||||||
|
expect(res.body.result).to.equal(`error`)
|
||||||
|
expect(res.body.error).to.equal(`mfaActive`)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe(`${store.icon('mfa')} Multi-Factor Authentication (MFA) login flow`, () => {
|
||||||
|
it(`${store.icon('mfa')} Should not login with username/password only`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.post('/login')
|
||||||
|
.send({
|
||||||
|
username: store.account.username,
|
||||||
|
password: store.account.password,
|
||||||
|
})
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal('success')
|
||||||
|
expect(res.body.note).to.equal('mfaTokenRequired')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ export const setup = async () => {
|
||||||
},
|
},
|
||||||
icons: {
|
icons: {
|
||||||
user: '🧑 ',
|
user: '🧑 ',
|
||||||
|
mfa: '🔒 ',
|
||||||
jwt: '🎫 ',
|
jwt: '🎫 ',
|
||||||
key: '🎟️ ',
|
key: '🎟️ ',
|
||||||
person: '🧕 ',
|
person: '🧕 ',
|
||||||
|
|
97
yarn.lock
97
yarn.lock
|
@ -4066,6 +4066,44 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@octokit/openapi-types" "^13.11.0"
|
"@octokit/openapi-types" "^13.11.0"
|
||||||
|
|
||||||
|
"@otplib/core@^12.0.1":
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@otplib/core/-/core-12.0.1.tgz#73720a8cedce211fe5b3f683cd5a9c098eaf0f8d"
|
||||||
|
integrity sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==
|
||||||
|
|
||||||
|
"@otplib/plugin-crypto@^12.0.1":
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz#2b42c624227f4f9303c1c041fca399eddcbae25e"
|
||||||
|
integrity sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==
|
||||||
|
dependencies:
|
||||||
|
"@otplib/core" "^12.0.1"
|
||||||
|
|
||||||
|
"@otplib/plugin-thirty-two@^12.0.1":
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz#5cc9b56e6e89f2a1fe4a2b38900ca4e11c87aa9e"
|
||||||
|
integrity sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==
|
||||||
|
dependencies:
|
||||||
|
"@otplib/core" "^12.0.1"
|
||||||
|
thirty-two "^1.0.2"
|
||||||
|
|
||||||
|
"@otplib/preset-default@^12.0.1":
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@otplib/preset-default/-/preset-default-12.0.1.tgz#cb596553c08251e71b187ada4a2246ad2a3165ba"
|
||||||
|
integrity sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==
|
||||||
|
dependencies:
|
||||||
|
"@otplib/core" "^12.0.1"
|
||||||
|
"@otplib/plugin-crypto" "^12.0.1"
|
||||||
|
"@otplib/plugin-thirty-two" "^12.0.1"
|
||||||
|
|
||||||
|
"@otplib/preset-v11@^12.0.1":
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@otplib/preset-v11/-/preset-v11-12.0.1.tgz#4c7266712e7230500b421ba89252963c838fc96d"
|
||||||
|
integrity sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==
|
||||||
|
dependencies:
|
||||||
|
"@otplib/core" "^12.0.1"
|
||||||
|
"@otplib/plugin-crypto" "^12.0.1"
|
||||||
|
"@otplib/plugin-thirty-two" "^12.0.1"
|
||||||
|
|
||||||
"@parcel/watcher@2.0.4":
|
"@parcel/watcher@2.0.4":
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b"
|
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b"
|
||||||
|
@ -4138,10 +4176,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452.tgz#5b7fae294ee9bd9790d0e7b7a0b0912e4222ac08"
|
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.5.0-43.0362da9eebca54d94c8ef5edd3b2e90af99ba452.tgz#5b7fae294ee9bd9790d0e7b7a0b0912e4222ac08"
|
||||||
integrity sha512-o7LyVx8PPJBLrEzLl6lpxxk2D5VnlM4Fwmrbq0NoT6pr5aa1OuHD9ZG+WJY6TlR/iD9bhmo2LNcxddCMr5Rv2A==
|
integrity sha512-o7LyVx8PPJBLrEzLl6lpxxk2D5VnlM4Fwmrbq0NoT6pr5aa1OuHD9ZG+WJY6TlR/iD9bhmo2LNcxddCMr5Rv2A==
|
||||||
|
|
||||||
"@prisma/engines@4.6.1":
|
"@prisma/engines@4.5.0":
|
||||||
version "4.6.1"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.6.1.tgz#ae31309cc0f600f2da22708697b3be4eb1e46f9e"
|
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.5.0.tgz#82df347a893a5ae2a67707d44772ba181f4b9328"
|
||||||
integrity sha512-3u2/XxvxB+Q7cMXHnKU0CpBiUK1QWqpgiBv28YDo1zOIJE3FCF8DI2vrp6vuwjGt5h0JGXDSvmSf4D4maVjJdw==
|
integrity sha512-4t9ir2SbQQr/wMCNU4YpHWp5hU14J2m3wHUZnGJPpmBF8YtkisxyVyQsKd1e6FyLTaGq8LOLhm6VLYHKqKNm+g==
|
||||||
|
|
||||||
"@reach/auto-id@^0.13.2":
|
"@reach/auto-id@^0.13.2":
|
||||||
version "0.13.2"
|
version "0.13.2"
|
||||||
|
@ -9838,6 +9876,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@^2.2.2:
|
dir-glob@^2.2.2:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
|
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
|
||||||
|
@ -10221,6 +10264,11 @@ enabled@2.0.x:
|
||||||
resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
|
resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
|
||||||
integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
|
integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
|
||||||
|
|
||||||
|
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.1, encodeurl@~1.0.2:
|
encodeurl@~1.0.1, 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"
|
||||||
|
@ -18846,6 +18894,15 @@ os-tmpdir@~1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||||
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
|
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
|
||||||
|
|
||||||
|
otplib@^12.0.1:
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/otplib/-/otplib-12.0.1.tgz#c1d3060ab7aadf041ed2960302f27095777d1f73"
|
||||||
|
integrity sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==
|
||||||
|
dependencies:
|
||||||
|
"@otplib/core" "^12.0.1"
|
||||||
|
"@otplib/preset-default" "^12.0.1"
|
||||||
|
"@otplib/preset-v11" "^12.0.1"
|
||||||
|
|
||||||
ow@^0.21.0:
|
ow@^0.21.0:
|
||||||
version "0.21.0"
|
version "0.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/ow/-/ow-0.21.0.tgz#c2df2ad78d1bfc2ea9cdca311b7a6275258df621"
|
resolved "https://registry.yarnpkg.com/ow/-/ow-0.21.0.tgz#c2df2ad78d1bfc2ea9cdca311b7a6275258df621"
|
||||||
|
@ -19775,6 +19832,11 @@ png-js@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d"
|
resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d"
|
||||||
integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==
|
integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==
|
||||||
|
|
||||||
|
pngjs@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
|
||||||
|
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
||||||
|
|
||||||
polished@^4.0.5:
|
polished@^4.0.5:
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
|
resolved "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
|
||||||
|
@ -20858,12 +20920,12 @@ prettyjson@^1.2.1:
|
||||||
colors "1.4.0"
|
colors "1.4.0"
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
|
|
||||||
prisma@4.6.1:
|
prisma@4.5.0:
|
||||||
version "4.6.1"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.6.1.tgz#6c85fb667abed006a6b849c9c1ddd81d3f071b87"
|
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.5.0.tgz#361ae3f4476d0821b97645e5da42975a7c2943bb"
|
||||||
integrity sha512-BR4itMCuzrDV4tn3e2TF+nh1zIX/RVU0isKtKoN28ADeoJ9nYaMhiuRRkFd2TZN8+l/XfYzoRKyHzUFXLQhmBQ==
|
integrity sha512-9Aeg4qiKlv9Wsjz4NO8k2CzRzlvS3A4FYVJ5+28sBBZ0eEwbiVOE/Jj7v6rZC1tFW2s4GSICQOAyuOjc6WsNew==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines" "4.6.1"
|
"@prisma/engines" "4.5.0"
|
||||||
|
|
||||||
prismjs@~1.27.0:
|
prismjs@~1.27.0:
|
||||||
version "1.27.0"
|
version "1.27.0"
|
||||||
|
@ -21105,6 +21167,16 @@ q@^1.1.2, q@^1.5.1:
|
||||||
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 sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==
|
integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==
|
||||||
|
|
||||||
|
qrcode@^1.5.1:
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.1.tgz#0103f97317409f7bc91772ef30793a54cd59f0cb"
|
||||||
|
integrity sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==
|
||||||
|
dependencies:
|
||||||
|
dijkstrajs "^1.0.1"
|
||||||
|
encode-utf8 "^1.0.3"
|
||||||
|
pngjs "^5.0.0"
|
||||||
|
yargs "^15.3.1"
|
||||||
|
|
||||||
qs@6.11.0, qs@^6.5.1, qs@^6.9.6:
|
qs@6.11.0, qs@^6.5.1, qs@^6.9.6:
|
||||||
version "6.11.0"
|
version "6.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
|
||||||
|
@ -24720,6 +24792,11 @@ thenify-all@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
any-promise "^1.0.0"
|
any-promise "^1.0.0"
|
||||||
|
|
||||||
|
thirty-two@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
|
||||||
|
integrity sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==
|
||||||
|
|
||||||
thread-stream@^2.0.0:
|
thread-stream@^2.0.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8"
|
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8"
|
||||||
|
@ -26677,7 +26754,7 @@ yargs@17.1.1:
|
||||||
y18n "^5.0.5"
|
y18n "^5.0.5"
|
||||||
yargs-parser "^20.2.2"
|
yargs-parser "^20.2.2"
|
||||||
|
|
||||||
yargs@^15.0.1, yargs@^15.0.2:
|
yargs@^15.0.1, yargs@^15.0.2, yargs@^15.3.1:
|
||||||
version "15.4.1"
|
version "15.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||||
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue