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
|
||||
|
||||
|
||||
#####################################################################
|
||||
# Multi-Factor Authentication (MFA) #
|
||||
#####################################################################
|
||||
|
||||
# The service for the token generator (think Google Authenticator)
|
||||
#BACKEND_MFA_SERVICE=FreeSewing
|
||||
|
||||
|
||||
#####################################################################
|
||||
# Email (via AWS SES) #
|
||||
# #
|
||||
|
|
|
@ -29,10 +29,12 @@
|
|||
"crypto": "^1.0.1",
|
||||
"express": "4.18.2",
|
||||
"mustache": "^4.2.0",
|
||||
"otplib": "^12.0.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"pino": "^8.7.0"
|
||||
"pino": "^8.7.0",
|
||||
"qrcode": "^1.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai-http": "^4.3.0",
|
||||
|
|
|
@ -55,6 +55,8 @@ model User {
|
|||
language String @default("en")
|
||||
lastLogin DateTime?
|
||||
lusername String @unique
|
||||
mfaSecret String @default("")
|
||||
mfaEnabled Boolean @default(false)
|
||||
newsletter Boolean @default(false)
|
||||
password String
|
||||
patron Int @default(0)
|
||||
|
|
|
@ -58,6 +58,9 @@ const config = {
|
|||
},
|
||||
languages: ['en', 'de', 'es', 'fr', 'nl'],
|
||||
measies: measurements,
|
||||
mfa: {
|
||||
service: process.env.BACKEND_MFA_SERVICE || 'FreeSewing',
|
||||
},
|
||||
port,
|
||||
roles: {
|
||||
levels: {
|
||||
|
|
|
@ -65,3 +65,16 @@ UsersController.prototype.update = async (req, res, tools) => {
|
|||
|
||||
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'
|
||||
// Encryption
|
||||
import { encryption } from './utils/crypto.mjs'
|
||||
// Multi-Factor Authentication (MFA)
|
||||
import { mfa } from './utils/mfa.mjs'
|
||||
// Email
|
||||
import { mailer } from './utils/email.mjs'
|
||||
|
||||
|
@ -28,6 +30,7 @@ const tools = {
|
|||
passport,
|
||||
prisma,
|
||||
...encryption(config.encryption.key),
|
||||
...mfa(config.mfa),
|
||||
...mailer(config),
|
||||
config,
|
||||
}
|
||||
|
|
|
@ -10,9 +10,10 @@ export function UserModel(tools) {
|
|||
this.prisma = tools.prisma
|
||||
this.decrypt = tools.decrypt
|
||||
this.encrypt = tools.encrypt
|
||||
this.mfa = tools.mfa
|
||||
this.mailer = tools.email
|
||||
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
|
||||
|
||||
return this
|
||||
|
@ -163,6 +164,8 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
|
|||
username,
|
||||
lusername: username,
|
||||
language: body.language,
|
||||
mfaEnabled: false,
|
||||
mfaSecret: this.encrypt(''),
|
||||
password: asJson(hashPassword(randomString())), // We'll change this later
|
||||
github: this.encrypt(''),
|
||||
bio: this.encrypt(''),
|
||||
|
@ -240,9 +243,16 @@ UserModel.prototype.passwordLogin = async function (req) {
|
|||
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
|
||||
log.info(`Login by user ${this.record.id} (${this.record.username})`)
|
||||
if (updatedPasswordField) {
|
||||
// Update the password field with a v3 hash
|
||||
await this.unguardedUpdate({ password: updatedPasswordField })
|
||||
|
@ -420,6 +430,59 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
|
|||
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
|
||||
*/
|
||||
|
|
|
@ -17,7 +17,6 @@ export function usersRoutes(tools) {
|
|||
app.post('/login', (req, res) => Users.login(req, res, tools))
|
||||
|
||||
// Read current jwt
|
||||
|
||||
app.get('/whoami/jwt', passport.authenticate(...jwt), (req, res) => Users.whoami(req, res, tools))
|
||||
app.get('/account/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Users.whoami(req, res, tools)
|
||||
|
@ -34,6 +33,13 @@ export function usersRoutes(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
|
||||
|
|
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 { mfaTests } from './mfa.mjs'
|
||||
import { accountTests } from './account.mjs'
|
||||
import { apikeyTests } from './apikey.mjs'
|
||||
import { personTests } from './person.mjs'
|
||||
|
@ -7,6 +8,7 @@ import { setup } from './shared.mjs'
|
|||
|
||||
const runTests = async (...params) => {
|
||||
await userTests(...params)
|
||||
await mfaTests(...params)
|
||||
await apikeyTests(...params)
|
||||
await accountTests(...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: {
|
||||
user: '🧑 ',
|
||||
mfa: '🔒 ',
|
||||
jwt: '🎫 ',
|
||||
key: '🎟️ ',
|
||||
person: '🧕 ',
|
||||
|
|
97
yarn.lock
97
yarn.lock
|
@ -4066,6 +4066,44 @@
|
|||
dependencies:
|
||||
"@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":
|
||||
version "2.0.4"
|
||||
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"
|
||||
integrity sha512-o7LyVx8PPJBLrEzLl6lpxxk2D5VnlM4Fwmrbq0NoT6pr5aa1OuHD9ZG+WJY6TlR/iD9bhmo2LNcxddCMr5Rv2A==
|
||||
|
||||
"@prisma/engines@4.6.1":
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.6.1.tgz#ae31309cc0f600f2da22708697b3be4eb1e46f9e"
|
||||
integrity sha512-3u2/XxvxB+Q7cMXHnKU0CpBiUK1QWqpgiBv28YDo1zOIJE3FCF8DI2vrp6vuwjGt5h0JGXDSvmSf4D4maVjJdw==
|
||||
"@prisma/engines@4.5.0":
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.5.0.tgz#82df347a893a5ae2a67707d44772ba181f4b9328"
|
||||
integrity sha512-4t9ir2SbQQr/wMCNU4YpHWp5hU14J2m3wHUZnGJPpmBF8YtkisxyVyQsKd1e6FyLTaGq8LOLhm6VLYHKqKNm+g==
|
||||
|
||||
"@reach/auto-id@^0.13.2":
|
||||
version "0.13.2"
|
||||
|
@ -9838,6 +9876,11 @@ diffie-hellman@^5.0.0:
|
|||
miller-rabin "^4.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:
|
||||
version "2.2.2"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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:
|
||||
version "0.21.0"
|
||||
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"
|
||||
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:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
|
||||
|
@ -20858,12 +20920,12 @@ prettyjson@^1.2.1:
|
|||
colors "1.4.0"
|
||||
minimist "^1.2.0"
|
||||
|
||||
prisma@4.6.1:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.6.1.tgz#6c85fb667abed006a6b849c9c1ddd81d3f071b87"
|
||||
integrity sha512-BR4itMCuzrDV4tn3e2TF+nh1zIX/RVU0isKtKoN28ADeoJ9nYaMhiuRRkFd2TZN8+l/XfYzoRKyHzUFXLQhmBQ==
|
||||
prisma@4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.5.0.tgz#361ae3f4476d0821b97645e5da42975a7c2943bb"
|
||||
integrity sha512-9Aeg4qiKlv9Wsjz4NO8k2CzRzlvS3A4FYVJ5+28sBBZ0eEwbiVOE/Jj7v6rZC1tFW2s4GSICQOAyuOjc6WsNew==
|
||||
dependencies:
|
||||
"@prisma/engines" "4.6.1"
|
||||
"@prisma/engines" "4.5.0"
|
||||
|
||||
prismjs@~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"
|
||||
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:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
|
||||
|
@ -24720,6 +24792,11 @@ thenify-all@^1.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.2.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue