feat: passkeys (add/remove)

pull/6775/head
NGPixel 1 year ago
parent a181579746
commit 4d285caaa7
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -344,6 +344,7 @@ export async function up (knex) {
table.string('name').notNullable()
table.jsonb('auth').notNullable().defaultTo('{}')
table.jsonb('meta').notNullable().defaultTo('{}')
table.jsonb('passkeys').notNullable().defaultTo('{}')
table.jsonb('prefs').notNullable().defaultTo('{}')
table.boolean('hasAvatar').notNullable().defaultTo(false)
table.boolean('isSystem').notNullable().defaultTo(false)

@ -3,6 +3,8 @@ import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import jwt from 'jsonwebtoken'
import ms from 'ms'
import { DateTime } from 'luxon'
import { v4 as uuid } from 'uuid'
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server'
export default {
Query: {
@ -122,6 +124,52 @@ export default {
return generateError(err)
}
},
/**
* Setup TFA
*/
async setupTFA (obj, args, context) {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
if (!usr) {
throw new Error('ERR_INVALID_USER')
}
const str = WIKI.auth.strategies[args.strategyId]
if (!str) {
throw new Error('ERR_INVALID_STRATEGY')
}
if (!usr.auth[args.strategyId]) {
throw new Error('ERR_INVALID_STRATEGY')
}
if (usr.auth[args.strategyId].tfaIsActive) {
throw new Error('ERR_TFA_ALREADY_ACTIVE')
}
const tfaQRImage = await usr.generateTFA(args.strategyId, args.siteId)
const tfaToken = await WIKI.db.userKeys.generateToken({
kind: 'tfaSetup',
userId: usr.id,
meta: {
strategyId: args.strategyId
}
})
return {
operation: generateSuccess('TFA setup started'),
continuationToken: tfaToken,
tfaQRImage
}
} catch (err) {
return generateError(err)
}
},
/**
* Deactivate 2FA
*/
@ -164,6 +212,158 @@ export default {
return generateError(err)
}
},
/**
* Setup Passkey
*/
async setupPasskey (obj, args, context) {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
if (!usr) {
throw new Error('ERR_INVALID_USER')
}
const site = WIKI.sites[args.siteId]
if (!site) {
throw new Error('ERR_INVALID_SITE')
} else if (site.hostname === '*') {
WIKI.logger.warn('Cannot use passkeys with a wildcard site hostname. Enter a valid hostname under the Administration Area > General.')
throw new Error('ERR_PK_HOSTNAME_MISSING')
}
const options = await generateRegistrationOptions({
rpName: site.config.title,
rpId: site.hostname,
userID: usr.id,
userName: usr.email,
userDisplayName: usr.name,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'required',
userVerification: 'preferred'
},
excludeCredentials: usr.passkeys.authenticators?.map(authenticator => ({
id: new Uint8Array(authenticator.credentialID),
type: 'public-key',
transports: authenticator.transports
})) ?? []
})
usr.passkeys.reg = {
challenge: options.challenge,
rpId: site.hostname,
siteId: site.id
}
await usr.$query().patch({
passkeys: usr.passkeys
})
return {
operation: generateSuccess('Passkey registration options generated successfully.'),
registrationOptions: options
}
} catch (err) {
return generateError(err)
}
},
/**
* Finalize Passkey Registration
*/
async finalizePasskey (obj, args, context) {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
if (!usr) {
throw new Error('ERR_INVALID_USER')
} else if (!usr.passkeys?.reg) {
throw new Error('ERR_PASSKEY_NOT_SETUP')
}
if (!args.name || args.name.trim().length < 1 || args.name.length > 255) {
throw new Error('ERR_PK_NAME_MISSING_OR_INVALID')
}
const verification = await verifyRegistrationResponse({
response: args.registrationResponse,
expectedChallenge: usr.passkeys.reg.challenge,
expectedOrigin: `https://${usr.passkeys.reg.rpId}`,
expectedRPID: usr.passkeys.reg.rpId,
requireUserVerification: true
})
if (!verification.verified) {
throw new Error('ERR_PK_VERIFICATION_FAILED')
}
if (!usr.passkeys.authenticators) {
usr.passkeys.authenticators = []
}
usr.passkeys.authenticators.push({
...verification.registrationInfo,
id: uuid(),
createdAt: new Date(),
name: args.name,
siteId: usr.passkeys.reg.siteId,
transports: args.registrationResponse.response.transports
})
delete usr.passkeys.reg
await usr.$query().patch({
passkeys: JSON.stringify(usr.passkeys, (k, v) => {
if (v instanceof Uint8Array) {
return Array.apply([], v)
}
return v
})
})
return {
operation: generateSuccess('Passkey registered successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* Deactivate a passkey
*/
async deactivatePasskey (obj, args, context) {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
if (!usr) {
throw new Error('ERR_INVALID_USER')
} else if (!usr.passkeys?.authenticators) {
throw new Error('ERR_PASSKEY_NOT_SETUP')
}
usr.passkeys.authenticators = usr.passkeys.authenticators.filter(a => a.id !== args.id)
await usr.$query().patch({
passkeys: usr.passkeys
})
return {
operation: generateSuccess('Passkey deactivated successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* Perform Password Change
*/

@ -2,6 +2,7 @@ import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import _, { isNil } from 'lodash-es'
import path from 'node:path'
import fs from 'fs-extra'
import { DateTime } from 'luxon'
export default {
Query: {
@ -59,6 +60,13 @@ export default {
return auth
})
usr.passkeys = usr.passkeys.authenticators?.map(a => ({
id: a.id,
createdAt: DateTime.fromISO(a.createdAt).toJSDate(),
name: a.name,
siteHostname: a.rpID
})) ?? []
return usr
},
// async profile (obj, args, context, info) {

@ -41,10 +41,28 @@ extend type Mutation {
setup: Boolean
): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
setupTFA(
strategyId: UUID!
siteId: UUID!
): AuthenticationSetupTFAResponse
deactivateTFA(
strategyId: UUID!
): DefaultResponse
setupPasskey(
siteId: UUID!
): AuthenticationSetupPasskeyResponse
finalizePasskey(
registrationResponse: JSON!
name: String!
): DefaultResponse
deactivatePasskey(
id: UUID!
): DefaultResponse
changePassword(
continuationToken: String
currentPassword: String
@ -135,6 +153,17 @@ type AuthenticationTokenResponse {
jwt: String
}
type AuthenticationSetupTFAResponse {
operation: Operation
continuationToken: String
tfaQRImage: String
}
type AuthenticationSetupPasskeyResponse {
operation: Operation
registrationOptions: JSON
}
input AuthenticationStrategyInput {
key: String!
strategyKey: String!

@ -132,6 +132,7 @@ type User {
name: String
email: String
auth: [UserAuth]
passkeys: [UserPasskey]
hasAvatar: Boolean
isSystem: Boolean
isActive: Boolean
@ -152,6 +153,13 @@ type UserAuth {
config: JSON
}
type UserPasskey {
id: UUID
name: String
createdAt: Date
siteHostname: String
}
type UserDefaults {
timezone: String
dateFormat: String

@ -1613,6 +1613,9 @@
"editor.unsaved.body": "You have unsaved changes. Are you sure you want to leave the editor and discard any modifications you made since the last save?",
"editor.unsaved.title": "Discard Unsaved Changes?",
"editor.unsavedWarning": "You have unsaved edits. Are you sure you want to leave the editor?",
"error.ERR_PK_ALREADY_REGISTERED": "It looks like this authenticator is already registered.",
"error.ERR_PK_HOSTNAME_MISSING": "Your administrator must set a valid site hostname before passkeys can be used.",
"error.ERR_PK_USER_CANCELLED": "Passkey registration aborted. Make sure to remove the key from your device.",
"fileman.7zFileType": "7zip Archive",
"fileman.aacFileType": "AAC Audio File",
"fileman.aiFileType": "Adobe Illustrator Document",
@ -1755,6 +1758,7 @@
"profile.authLoadingFailed": "Failed to load authentication methods.",
"profile.authModifyTfa": "Modify 2FA",
"profile.authSetTfa": "Set 2FA",
"profile.authSetTfaLoading": "Setting up 2FA... Please wait",
"profile.avatar": "Avatar",
"profile.avatarClearFailed": "Failed to clear profile picture.",
"profile.avatarClearSuccess": "Profile picture cleared successfully.",
@ -1798,6 +1802,18 @@
"profile.pages.refreshSuccess": "Page list has been refreshed.",
"profile.pages.subtitle": "List of pages I created or last modified",
"profile.pages.title": "Pages",
"profile.passkeys": "Passkeys",
"profile.passkeysAdd": "Add Passkey",
"profile.passkeysDeactivateConfirm": "Are you sure you want to deactivate this passkey?",
"profile.passkeysDeactivateFailed": "Failed to deactivate the passkey.",
"profile.passkeysDeactivateSuccess": "Passkey deactivated successfully. You may still need to remove the passkey from your device.",
"profile.passkeysIntro": "Passkeys are a replacement for passwords for a faster, easier and more secure login. It relies on your device existing biometrics (phone, computer, security key) to validate your identity.",
"profile.passkeysInvalidName": "Passkey name is missing or invalid.",
"profile.passkeysName": "Passkey Name",
"profile.passkeysNameHint": "Enter a name for your passkey:",
"profile.passkeysSetupFailed": "Failed to setup new passkey.",
"profile.passkeysSetupSuccess": "Passkey registered successfully.",
"profile.passkeysUnsupported": "Passkeys are not supported on your device.",
"profile.preferences": "Preferences",
"profile.pronouns": "Pronouns",
"profile.pronounsHint": "Let people know which pronouns should they use when referring to you.",

@ -485,7 +485,7 @@ export class User extends Model {
}
if (user) {
user.auth[strategyId].password = await bcrypt.hash(newPassword, 12),
user.auth[strategyId].password = await bcrypt.hash(newPassword, 12)
user.auth[strategyId].mustChangePwd = false
await user.$query().patch({
auth: user.auth

@ -1,30 +1,30 @@
const request = require('request-promise')
// TODO: refactor to use fetch()
const prefetch = async (element) => {
const url = element.attr(`src`)
let response
try {
response = await request({
method: `GET`,
url,
resolveWithFullResponse: true
})
} catch (err) {
WIKI.logger.warn(`Failed to prefetch ${url}`)
WIKI.logger.warn(err)
return
}
const contentType = response.headers[`content-type`]
const image = Buffer.from(response.body).toString('base64')
element.attr('src', `data:${contentType};base64,${image}`)
element.removeClass('prefetch-candidate')
}
// const prefetch = async (element) => {
// const url = element.attr(`src`)
// let response
// try {
// response = await request({
// method: `GET`,
// url,
// resolveWithFullResponse: true
// })
// } catch (err) {
// WIKI.logger.warn(`Failed to prefetch ${url}`)
// WIKI.logger.warn(err)
// return
// }
// const contentType = response.headers[`content-type`]
// const image = Buffer.from(response.body).toString('base64')
// element.attr('src', `data:${contentType};base64,${image}`)
// element.removeClass('prefetch-candidate')
// }
module.exports = {
async init($) {
const promises = $('img.prefetch-candidate').map((index, element) => {
return prefetch($(element))
}).toArray()
await Promise.all(promises)
// const promises = $('img.prefetch-candidate').map((index, element) => {
// return prefetch($(element))
// }).toArray()
// await Promise.all(promises)
}
}

@ -42,9 +42,11 @@
"@graphql-tools/schema": "10.0.0",
"@graphql-tools/utils": "10.0.6",
"@joplin/turndown-plugin-gfm": "1.0.50",
"@node-saml/passport-saml": "4.0.4",
"@root/csr": "0.8.1",
"@root/keypairs": "0.10.3",
"@root/pem": "1.0.4",
"@simplewebauthn/server": "8.2.0",
"acme": "3.0.3",
"akismet-api": "6.0.0",
"aws-sdk": "2.1472.0",
@ -83,8 +85,6 @@
"graphql-upload": "16.0.2",
"he": "1.2.0",
"highlight.js": "11.8.0",
"i18next": "23.5.1",
"i18next-node-fs-backend": "2.1.3",
"image-size": "1.0.2",
"js-base64": "3.7.5",
"js-binary": "1.2.0",
@ -138,7 +138,6 @@
"passport-oauth2": "1.7.0",
"passport-okta-oauth": "0.0.1",
"passport-openidconnect": "0.1.1",
"passport-saml": "3.2.4",
"passport-slack-oauth2": "1.2.0",
"passport-twitch-strategy": "2.2.0",
"pem-jwk": "2.0.0",
@ -152,8 +151,6 @@
"puppeteer-core": "21.3.8",
"qr-image": "3.2.0",
"remove-markdown": "0.5.0",
"request": "2.88.2",
"request-promise": "4.2.6",
"safe-regex": "2.1.1",
"sanitize-filename": "1.6.3",
"scim-query-filter-parser": "2.0.4",
@ -179,7 +176,6 @@
"eslint-plugin-import": "2.28.1",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-standard": "5.0.0",
"nodemon": "3.0.1"
},
"overrides": {

@ -23,6 +23,9 @@ dependencies:
'@joplin/turndown-plugin-gfm':
specifier: 1.0.50
version: 1.0.50
'@node-saml/passport-saml':
specifier: 4.0.4
version: 4.0.4
'@root/csr':
specifier: 0.8.1
version: 0.8.1
@ -32,6 +35,9 @@ dependencies:
'@root/pem':
specifier: 1.0.4
version: 1.0.4
'@simplewebauthn/server':
specifier: 8.2.0
version: 8.2.0
acme:
specifier: 3.0.3
version: 3.0.3
@ -146,12 +152,6 @@ dependencies:
highlight.js:
specifier: 11.8.0
version: 11.8.0
i18next:
specifier: 23.5.1
version: 23.5.1
i18next-node-fs-backend:
specifier: 2.1.3
version: 2.1.3
image-size:
specifier: 1.0.2
version: 1.0.2
@ -311,9 +311,6 @@ dependencies:
passport-openidconnect:
specifier: 0.1.1
version: 0.1.1
passport-saml:
specifier: 3.2.4
version: 3.2.4
passport-slack-oauth2:
specifier: 1.2.0
version: 1.2.0
@ -353,12 +350,6 @@ dependencies:
remove-markdown:
specifier: 0.5.0
version: 0.5.0
request:
specifier: 2.88.2
version: 2.88.2
request-promise:
specifier: 4.2.6
version: 4.2.6(request@2.88.2)
safe-regex:
specifier: 2.1.1
version: 2.1.1
@ -430,9 +421,6 @@ devDependencies:
eslint-plugin-promise:
specifier: 6.1.1
version: 6.1.1(eslint@8.51.0)
eslint-plugin-standard:
specifier: 5.0.0
version: 5.0.0(eslint@8.51.0)
nodemon:
specifier: 3.0.1
version: 3.0.1
@ -754,12 +742,53 @@ packages:
- encoding
dev: false
/@babel/runtime@7.23.1:
resolution: {integrity: sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.0
/@cbor-extract/cbor-extract-darwin-arm64@2.1.1:
resolution: {integrity: sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@cbor-extract/cbor-extract-darwin-x64@2.1.1:
resolution: {integrity: sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@cbor-extract/cbor-extract-linux-arm64@2.1.1:
resolution: {integrity: sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@cbor-extract/cbor-extract-linux-arm@2.1.1:
resolution: {integrity: sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@cbor-extract/cbor-extract-linux-x64@2.1.1:
resolution: {integrity: sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@cbor-extract/cbor-extract-win32-x64@2.1.1:
resolution: {integrity: sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@eslint-community/eslint-utils@4.4.0(eslint@8.51.0):
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
@ -880,6 +909,10 @@ packages:
graphql: 16.8.1
dev: false
/@hexagon/base64@1.1.28:
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
dev: false
/@humanwhocodes/config-array@0.11.11:
resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==}
engines: {node: '>=10.10.0'}
@ -920,6 +953,39 @@ packages:
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
dev: false
/@node-saml/node-saml@4.0.5:
resolution: {integrity: sha512-J5DglElbY1tjOuaR1NPtjOXkXY5bpUhDoKVoeucYN98A3w4fwgjIOPqIGcb6cQsqFq2zZ6vTCeKn5C/hvefSaw==}
engines: {node: '>= 14'}
dependencies:
'@types/debug': 4.1.9
'@types/passport': 1.0.13
'@types/xml-crypto': 1.4.3
'@types/xml-encryption': 1.2.2
'@types/xml2js': 0.4.12
'@xmldom/xmldom': 0.8.10
debug: 4.3.4
xml-crypto: 3.2.0
xml-encryption: 3.0.2
xml2js: 0.5.0
xmlbuilder: 15.1.1
transitivePeerDependencies:
- supports-color
dev: false
/@node-saml/passport-saml@4.0.4:
resolution: {integrity: sha512-xFw3gw0yo+K1mzlkW15NeBF7cVpRHN/4vpjmBKzov5YFImCWh/G0LcTZ8krH3yk2/eRPc3Or8LRPudVJBjmYaw==}
engines: {node: '>= 14'}
dependencies:
'@node-saml/node-saml': 4.0.5
'@types/express': 4.17.18
'@types/passport': 1.0.13
'@types/passport-strategy': 0.2.36
passport: 0.6.0
passport-strategy: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -1194,6 +1260,50 @@ packages:
engines: {node: '>=8.0.0'}
dev: false
/@peculiar/asn1-android@2.3.6:
resolution: {integrity: sha512-zkYh4DsiRhiNfg6tWaUuRc+huwlb9XJbmeZLrjTz9v76UK1Ehq3EnfJFED6P3sdznW/nqWe46LoM9JrqxcD58g==}
dependencies:
'@peculiar/asn1-schema': 2.3.6
asn1js: 3.0.5
tslib: 2.6.2
dev: false
/@peculiar/asn1-ecc@2.3.6:
resolution: {integrity: sha512-Hu1xzMJQWv8/GvzOiinaE6XiD1/kEhq2C/V89UEoWeZ2fLUcGNIvMxOr/pMyL0OmpRWj/mhCTXOZp4PP+a0aTg==}
dependencies:
'@peculiar/asn1-schema': 2.3.6
'@peculiar/asn1-x509': 2.3.6
asn1js: 3.0.5
tslib: 2.6.2
dev: false
/@peculiar/asn1-rsa@2.3.6:
resolution: {integrity: sha512-DswjJyAXZnvESuImGNTvbNKvh1XApBVqU+r3UmrFFTAI23gv62byl0f5OFKWTNhCf66WQrd3sklpsCZc/4+jwA==}
dependencies:
'@peculiar/asn1-schema': 2.3.6
'@peculiar/asn1-x509': 2.3.6
asn1js: 3.0.5
tslib: 2.6.2
dev: false
/@peculiar/asn1-schema@2.3.6:
resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==}
dependencies:
asn1js: 3.0.5
pvtsutils: 1.3.5
tslib: 2.6.2
dev: false
/@peculiar/asn1-x509@2.3.6:
resolution: {integrity: sha512-dRwX31R1lcbIdzbztiMvLNTDoGptxdV7HocNx87LfKU0fEWh7fTWJjx4oV+glETSy6heF/hJHB2J4RGB3vVSYg==}
dependencies:
'@peculiar/asn1-schema': 2.3.6
asn1js: 3.0.5
ipaddr.js: 2.1.0
pvtsutils: 1.3.5
tslib: 2.6.2
dev: false
/@protobufjs/aspromise@1.1.2:
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
dev: false
@ -1306,6 +1416,27 @@ packages:
'@root/encoding': 1.0.1
dev: false
/@simplewebauthn/server@8.2.0:
resolution: {integrity: sha512-nknf7kCa5V61Kk2zn1vTuKeAlyut9aWduIcbHNQWpMCEJqH/m8cXpb+9UV42MEQRIk8JVC1GSNeEx56QVTfJHw==}
engines: {node: '>=16.0.0'}
dependencies:
'@hexagon/base64': 1.1.28
'@peculiar/asn1-android': 2.3.6
'@peculiar/asn1-ecc': 2.3.6
'@peculiar/asn1-rsa': 2.3.6
'@peculiar/asn1-schema': 2.3.6
'@peculiar/asn1-x509': 2.3.6
'@simplewebauthn/typescript-types': 8.0.0
cbor-x: 1.5.4
cross-fetch: 4.0.0
transitivePeerDependencies:
- encoding
dev: false
/@simplewebauthn/typescript-types@8.0.0:
resolution: {integrity: sha512-d7Izb2H+LZJteXMkS8DmpAarD6mZdpIOu/av/yH4/u/3Pd6DKFLyBM3j8BMmUvUqpzvJvHARNrRfQYto58mtTQ==}
dev: false
/@socket.io/component-emitter@3.1.0:
resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==}
dev: false
@ -1360,6 +1491,12 @@ packages:
'@types/node': 20.8.3
dev: false
/@types/debug@4.1.9:
resolution: {integrity: sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==}
dependencies:
'@types/ms': 0.7.32
dev: false
/@types/express-serve-static-core@4.17.37:
resolution: {integrity: sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==}
dependencies:
@ -1425,6 +1562,10 @@ packages:
resolution: {integrity: sha512-Wj+fqpTLtTbG7c0tH47dkahefpLKEbB+xAZuLq7b4/IDHPl/n6VoXcyUQ2bypFlbSwvCr0y+bD4euTTqTJsPxQ==}
dev: false
/@types/ms@0.7.32:
resolution: {integrity: sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g==}
dev: false
/@types/node-fetch@2.6.6:
resolution: {integrity: sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==}
dependencies:
@ -1446,6 +1587,19 @@ packages:
resolution: {integrity: sha512-STkyj0IQkgbmohF1afXQN64KucE3w7EgSbNJxqkJoq0KHVBV4nU5Pyku+TM9UCiCLXhZlkEFd8zq38P8lDFi6g==}
dev: false
/@types/passport-strategy@0.2.36:
resolution: {integrity: sha512-hotVZuaCt04LJYXfZD5B+5UeCcRVG8IjKaLLGTJ1eFp0wiFQA2XfsqslGGInWje+OysNNLPH/ducce5GXHDC1Q==}
dependencies:
'@types/express': 4.17.18
'@types/passport': 1.0.13
dev: false
/@types/passport@1.0.13:
resolution: {integrity: sha512-XXURryL+EZAWtbQFOHX1eNB+RJwz5XMPPz1xrGpEKr2xUZCXM4NCPkHMtZQ3B2tTSG/1IRaAcTHjczRA4sSFCw==}
dependencies:
'@types/express': 4.17.18
dev: false
/@types/qs@6.9.8:
resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==}
dev: false
@ -1475,6 +1629,25 @@ packages:
'@types/node': 20.8.3
dev: false
/@types/xml-crypto@1.4.3:
resolution: {integrity: sha512-pnvKYb7vUsUIMc+C6JM/j779YWQgOMcwjnqHJ9cdaWXwWEBE1hAqthzeszRx62V5RWMvS+XS9w9tXMOYyUc8zg==}
dependencies:
'@types/node': 20.8.3
xpath: 0.0.27
dev: false
/@types/xml-encryption@1.2.2:
resolution: {integrity: sha512-UeuYOqW3ZzUQfwb/mb3GNZ2/DlVdh5mjJNmB/yFXgQr8/pwlVJ9I2w+AHPfRDzLshe7YpgUB4T1//qgbk6U87Q==}
dependencies:
'@types/node': 20.8.3
dev: false
/@types/xml2js@0.4.12:
resolution: {integrity: sha512-CZPpQKBZ8db66EP5hCjwvYrLThgZvnyZrPXK2W+UI1oOaWezGt34iOaUCX4Jah2X8+rQqjvl9VKEIT8TR1I0rA==}
dependencies:
'@types/node': 20.8.3
dev: false
/@types/yauzl@2.10.1:
resolution: {integrity: sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==}
requiresBuild: true
@ -1519,8 +1692,8 @@ packages:
dev: false
optional: true
/@xmldom/xmldom@0.7.13:
resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==}
/@xmldom/xmldom@0.8.10:
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
engines: {node: '>=10.0.0'}
dev: false
@ -1607,6 +1780,7 @@ packages:
fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1
uri-js: 4.4.1
dev: true
/ajv@8.12.0:
resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
@ -1658,12 +1832,6 @@ packages:
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
dev: false
/argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
dependencies:
sprintf-js: 1.0.3
dev: false
/argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@ -1752,6 +1920,15 @@ packages:
safer-buffer: 2.1.2
dev: false
/asn1js@3.0.5:
resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==}
engines: {node: '>=12.0.0'}
dependencies:
pvtsutils: 1.3.5
pvutils: 1.1.3
tslib: 2.6.2
dev: false
/assert-plus@1.0.0:
resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
engines: {node: '>=0.8'}
@ -1802,14 +1979,6 @@ packages:
xml2js: 0.5.0
dev: false
/aws-sign2@0.7.0:
resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
dev: false
/aws4@1.12.0:
resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==}
dev: false
/axios@0.27.2:
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
dependencies:
@ -1852,12 +2021,6 @@ packages:
engines: {node: '>=10.0.0'}
dev: false
/bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
dependencies:
tweetnacl: 0.14.5
dev: false
/bcryptjs@2.4.3:
resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==}
dev: false
@ -2042,8 +2205,26 @@ packages:
engines: {node: '>=6'}
dev: true
/caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
/cbor-extract@2.1.1:
resolution: {integrity: sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA==}
hasBin: true
requiresBuild: true
dependencies:
node-gyp-build-optional-packages: 5.0.3
optionalDependencies:
'@cbor-extract/cbor-extract-darwin-arm64': 2.1.1
'@cbor-extract/cbor-extract-darwin-x64': 2.1.1
'@cbor-extract/cbor-extract-linux-arm': 2.1.1
'@cbor-extract/cbor-extract-linux-arm64': 2.1.1
'@cbor-extract/cbor-extract-linux-x64': 2.1.1
'@cbor-extract/cbor-extract-win32-x64': 2.1.1
dev: false
optional: true
/cbor-x@1.5.4:
resolution: {integrity: sha512-PVKILDn+Rf6MRhhcyzGXi5eizn1i0i3F8Fe6UMMxXBnWkalq9+C5+VTmlIjAYM4iF2IYF2N+zToqAfYOp+3rfw==}
optionalDependencies:
cbor-extract: 2.1.1
dev: false
/chalk@4.1.2:
@ -2367,13 +2548,6 @@ packages:
resolution: {integrity: sha512-YOrdiS4b/ItbPAtyEIpkhqryoul2Bu8vtX+SN2nmxsqPnqAfh48Nu9p6zdTp9iCgCoSb6Ib8B0y4UUznaVXgtA==}
dev: false
/dashdash@1.14.1:
resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
engines: {node: '>=0.10'}
dependencies:
assert-plus: 1.0.0
dev: false
/data-uri-to-buffer@6.0.1:
resolution: {integrity: sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg==}
engines: {node: '>= 14'}
@ -2602,13 +2776,6 @@ packages:
dev: false
optional: true
/ecc-jsbn@0.1.2:
resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
dependencies:
jsbn: 0.1.1
safer-buffer: 2.1.2
dev: false
/ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
dependencies:
@ -3150,10 +3317,6 @@ packages:
- supports-color
dev: false
/extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
dev: false
/extract-zip@2.0.1:
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
engines: {node: '>= 10.17.0'}
@ -3187,6 +3350,7 @@ packages:
/fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
dev: true
/fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
@ -3286,19 +3450,6 @@ packages:
dependencies:
is-callable: 1.2.7
/forever-agent@0.6.1:
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
dev: false
/form-data@2.3.3:
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
engines: {node: '>= 0.12'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: false
/form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
@ -3443,12 +3594,6 @@ packages:
async: 3.2.4
dev: false
/getpass@0.1.7:
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
dependencies:
assert-plus: 1.0.0
dev: false
/github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
dev: false
@ -3595,20 +3740,6 @@ packages:
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
dev: false
/har-schema@2.0.0:
resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
engines: {node: '>=4'}
dev: false
/har-validator@5.1.5:
resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==}
engines: {node: '>=6'}
deprecated: this library is no longer supported
dependencies:
ajv: 6.12.6
har-schema: 2.0.0
dev: false
/has-bigints@1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
dev: true
@ -3726,15 +3857,6 @@ packages:
- supports-color
dev: false
/http-signature@1.2.0:
resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
engines: {node: '>=0.8', npm: '>=1.3.7'}
dependencies:
assert-plus: 1.0.0
jsprim: 1.4.2
sshpk: 1.17.0
dev: false
/https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
@ -3755,20 +3877,6 @@ packages:
- supports-color
dev: false
/i18next-node-fs-backend@2.1.3:
resolution: {integrity: sha512-CreMFiVl3ChlMc5ys/e0QfuLFOZyFcL40Jj6jaKD6DxZ/GCUMxPI9BpU43QMWUgC7r+PClpxg2cGXAl0CjG04g==}
deprecated: replaced by i18next-fs-backend
dependencies:
js-yaml: 3.13.1
json5: 2.0.0
dev: false
/i18next@23.5.1:
resolution: {integrity: sha512-JelYzcaCoFDaa+Ysbfz2JsGAKkrHiMG6S61+HLBUEIPaF40WMwW9hCPymlQGrP+wWawKxKPuSuD71WZscCsWHg==}
dependencies:
'@babel/runtime': 7.23.1
dev: false
/iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@ -3866,6 +3974,11 @@ packages:
engines: {node: '>= 0.10'}
dev: false
/ipaddr.js@2.1.0:
resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==}
engines: {node: '>= 10'}
dev: false
/is-arguments@1.1.1:
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
engines: {node: '>= 0.4'}
@ -4008,10 +4121,6 @@ packages:
dependencies:
which-typed-array: 1.1.11
/is-typedarray@1.0.0:
resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
dev: false
/is-weakref@1.0.2:
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
dependencies:
@ -4030,10 +4139,6 @@ packages:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
/isstream@0.1.2:
resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
dev: false
/jmespath@0.16.0:
resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==}
engines: {node: '>= 0.6.0'}
@ -4054,24 +4159,12 @@ packages:
dev: false
optional: true
/js-yaml@3.13.1:
resolution: {integrity: sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==}
hasBin: true
dependencies:
argparse: 1.0.10
esprima: 4.0.1
dev: false
/js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
dependencies:
argparse: 2.0.1
/jsbn@0.1.1:
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
dev: false
/jsdom@22.1.0:
resolution: {integrity: sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==}
engines: {node: '>=16'}
@ -4116,23 +4209,16 @@ packages:
/json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
dev: true
/json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
dev: false
/json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
dev: false
/json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true
/json-stringify-safe@5.0.1:
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
dev: false
/json5@1.0.2:
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true
@ -4140,14 +4226,6 @@ packages:
minimist: 1.2.8
dev: true
/json5@2.0.0:
resolution: {integrity: sha512-0EdQvHuLm7yJ7lyG5dp7Q3X2ku++BG5ZHaJ5FTnaXpKqDrw4pMxel5Bt3oAYMthnrthFBdnZ1FcsXTPyrQlV0w==}
engines: {node: '>=6'}
hasBin: true
dependencies:
minimist: 1.2.8
dev: false
/jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
optionalDependencies:
@ -4186,16 +4264,6 @@ packages:
semver: 7.5.4
dev: false
/jsprim@1.4.2:
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
engines: {node: '>=0.6.0'}
dependencies:
assert-plus: 1.0.0
extsprintf: 1.3.0
json-schema: 0.4.0
verror: 1.10.0
dev: false
/jwa@1.4.1:
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
dependencies:
@ -4763,6 +4831,13 @@ packages:
engines: {node: '>= 6.13.0'}
dev: false
/node-gyp-build-optional-packages@5.0.3:
resolution: {integrity: sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==}
hasBin: true
requiresBuild: true
dev: false
optional: true
/node-jose@2.2.0:
resolution: {integrity: sha512-XPCvJRr94SjLrSIm4pbYHKLEaOsDvJCpyFw/6V/KK/IXmyZ6SFBzAUDO9HQf4DB/nTEFcRGH87mNciOP23kFjw==}
dependencies:
@ -4829,10 +4904,6 @@ packages:
resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
dev: false
/oauth-sign@0.9.0:
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
dev: false
/oauth@0.9.15:
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
dev: false
@ -5194,22 +5265,6 @@ packages:
passport-strategy: 1.0.0
dev: false
/passport-saml@3.2.4:
resolution: {integrity: sha512-JSgkFXeaexLNQh1RrOvJAgjLnZzH/S3HbX/mWAk+i7aulnjqUe7WKnPl1NPnJWqP7Dqsv0I2Xm6KIFHkftk0HA==}
engines: {node: '>= 12'}
deprecated: For versions >= 4, please use scopped package @node-saml/passport-saml
dependencies:
'@xmldom/xmldom': 0.7.13
debug: 4.3.4
passport-strategy: 1.0.0
xml-crypto: 2.1.5
xml-encryption: 2.0.0
xml2js: 0.4.23
xmlbuilder: 15.1.1
transitivePeerDependencies:
- supports-color
dev: false
/passport-slack-oauth2@1.2.0:
resolution: {integrity: sha512-SeQl8uPoi4ajhzgIvwQM7gW/6yPrKH0hPFjxcP/426SOZ0M9ZNDOfSa32q3NTw7KcwYOTjyWX/2xdJndQE7Rkg==}
dependencies:
@ -5281,10 +5336,6 @@ packages:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
dev: false
/performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
dev: false
/pg-cloudflare@1.1.1:
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
requiresBuild: true
@ -5579,6 +5630,17 @@ packages:
- utf-8-validate
dev: false
/pvtsutils@1.3.5:
resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==}
dependencies:
tslib: 2.6.2
dev: false
/pvutils@1.1.3:
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
engines: {node: '>=6.0.0'}
dev: false
/qr-image@3.2.0:
resolution: {integrity: sha512-rXKDS5Sx3YipVsqmlMJsJsk6jXylEpiHRC2+nJy66fxA5ExYyGa4PqwteW69SaVmAb2OQ18HbYriT7cGQMbduw==}
dev: false
@ -5597,11 +5659,6 @@ packages:
side-channel: 1.0.4
dev: false
/qs@6.5.3:
resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
engines: {node: '>=0.6'}
dev: false
/querystring@0.2.0:
resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==}
engines: {node: '>=0.4.x'}
@ -5717,10 +5774,6 @@ packages:
resolve: 1.22.6
dev: false
/regenerator-runtime@0.14.0:
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
dev: false
/regexp-tree@0.1.27:
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
hasBin: true
@ -5744,57 +5797,6 @@ packages:
resolution: {integrity: sha512-x917M80K97K5IN1L8lUvFehsfhR8cYjGQ/yAMRI9E7JIKivtl5Emo5iD13DhMr+VojzMCiYk8V2byNPwT/oapg==}
dev: false
/request-promise-core@1.1.4(request@2.88.2):
resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==}
engines: {node: '>=0.10.0'}
peerDependencies:
request: ^2.34
dependencies:
lodash: 4.17.21
request: 2.88.2
dev: false
/request-promise@4.2.6(request@2.88.2):
resolution: {integrity: sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==}
engines: {node: '>=0.10.0'}
deprecated: request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142
peerDependencies:
request: ^2.34
dependencies:
bluebird: 3.7.2
request: 2.88.2
request-promise-core: 1.1.4(request@2.88.2)
stealthy-require: 1.1.1
tough-cookie: 2.5.0
dev: false
/request@2.88.2:
resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
engines: {node: '>= 6'}
deprecated: request has been deprecated, see https://github.com/request/request/issues/3142
dependencies:
aws-sign2: 0.7.0
aws4: 1.12.0
caseless: 0.12.0
combined-stream: 1.0.8
extend: 3.0.2
forever-agent: 0.6.1
form-data: 2.3.3
har-validator: 5.1.5
http-signature: 1.2.0
is-typedarray: 1.0.0
isstream: 0.1.2
json-stringify-safe: 5.0.1
mime-types: 2.1.35
oauth-sign: 0.9.0
performance-now: 2.1.0
qs: 6.5.3
safe-buffer: 5.2.1
tough-cookie: 2.5.0
tunnel-agent: 0.6.0
uuid: 3.4.0
dev: false
/require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@ -5934,10 +5936,6 @@ packages:
resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==}
dev: false
/sax@1.3.0:
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
dev: false
/saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
@ -6168,36 +6166,11 @@ packages:
engines: {node: '>= 10.x'}
dev: false
/sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
dev: false
/sshpk@1.17.0:
resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==}
engines: {node: '>=0.10.0'}
hasBin: true
dependencies:
asn1: 0.2.6
assert-plus: 1.0.0
bcrypt-pbkdf: 1.0.2
dashdash: 1.14.1
ecc-jsbn: 0.1.2
getpass: 0.1.7
jsbn: 0.1.1
safer-buffer: 2.1.2
tweetnacl: 0.14.5
dev: false
/statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
dev: false
/stealthy-require@1.1.1:
resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==}
engines: {node: '>=0.10.0'}
dev: false
/streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
@ -6421,14 +6394,6 @@ packages:
nopt: 1.0.10
dev: true
/tough-cookie@2.5.0:
resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
engines: {node: '>=0.8'}
dependencies:
psl: 1.9.0
punycode: 2.3.0
dev: false
/tough-cookie@4.1.3:
resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
engines: {node: '>=6'}
@ -6495,10 +6460,6 @@ packages:
domino: 2.1.6
dev: false
/tweetnacl@0.14.5:
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
dev: false
/twemoji-parser@14.0.0:
resolution: {integrity: sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==}
dev: false
@ -6714,12 +6675,6 @@ packages:
engines: {node: '>= 0.4.0'}
dev: false
/uuid@3.4.0:
resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
hasBin: true
dev: false
/uuid@8.0.0:
resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==}
hasBin: true
@ -6887,19 +6842,19 @@ packages:
optional: true
dev: false
/xml-crypto@2.1.5:
resolution: {integrity: sha512-xOSJmGFm+BTXmaPYk8pPV3duKo6hJuZ5niN4uMzoNcTlwYs0jAu/N3qY+ud9MhE4N7eMRuC1ayC7Yhmb7MmAWg==}
engines: {node: '>=0.4.0'}
/xml-crypto@3.2.0:
resolution: {integrity: sha512-qVurBUOQrmvlgmZqIVBqmb06TD2a/PpEUfFPgD7BuBfjmoH4zgkqaWSIJrnymlCvM2GGt9x+XtJFA+ttoAufqg==}
engines: {node: '>=4.0.0'}
dependencies:
'@xmldom/xmldom': 0.7.13
'@xmldom/xmldom': 0.8.10
xpath: 0.0.32
dev: false
/xml-encryption@2.0.0:
resolution: {integrity: sha512-4Av83DdvAgUQQMfi/w8G01aJshbEZP9ewjmZMpS9t3H+OCZBDvyK4GJPnHGfWiXlArnPbYvR58JB9qF2x9Ds+Q==}
/xml-encryption@3.0.2:
resolution: {integrity: sha512-VxYXPvsWB01/aqVLd6ZMPWZ+qaj0aIdF+cStrVJMcFj3iymwZeI0ABzB3VqMYv48DkSpRhnrXqTUkR34j+UDyg==}
engines: {node: '>=12'}
dependencies:
'@xmldom/xmldom': 0.7.13
'@xmldom/xmldom': 0.8.10
escape-html: 1.0.3
xpath: 0.0.32
dev: false
@ -6909,14 +6864,6 @@ packages:
engines: {node: '>=12'}
dev: false
/xml2js@0.4.23:
resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
engines: {node: '>=4.0.0'}
dependencies:
sax: 1.3.0
xmlbuilder: 11.0.1
dev: false
/xml2js@0.4.4:
resolution: {integrity: sha512-9ERdxLOo4EazMDHAS/vsuZiTXIMur6ydcRfzGrFVJ4qM78zD3ohUgPJC7NYpGwd5rnS0ufSydMJClh6jyH+V0w==}
dependencies:
@ -6946,6 +6893,11 @@ packages:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
dev: false
/xpath@0.0.27:
resolution: {integrity: sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==}
engines: {node: '>=0.6.0'}
dev: false
/xpath@0.0.32:
resolution: {integrity: sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==}
engines: {node: '>=0.6.0'}

@ -17,6 +17,7 @@
"@lezer/common": "1.1.0",
"@mdi/font": "7.3.67",
"@quasar/extras": "1.16.7",
"@simplewebauthn/browser": "8.3.1",
"@tiptap/core": "2.1.11",
"@tiptap/extension-code-block": "2.1.11",
"@tiptap/extension-code-block-lowlight": "2.1.11",

@ -17,6 +17,9 @@ dependencies:
'@quasar/extras':
specifier: 1.16.7
version: 1.16.7
'@simplewebauthn/browser':
specifier: 8.3.1
version: 8.3.1
'@tiptap/core':
specifier: 2.1.11
version: 2.1.11(@tiptap/pm@2.1.11)
@ -709,6 +712,16 @@ packages:
picomatch: 2.3.1
dev: true
/@simplewebauthn/browser@8.3.1:
resolution: {integrity: sha512-bMW7oOkxX4ydRAkkPtJ1do2k9yOoIGc/hZYebcuEOVdJoC6wwVpu97mYY7Mz8B9hLlcaR5WFgBsLl5tSJVzm8A==}
dependencies:
'@simplewebauthn/typescript-types': 8.0.0
dev: false
/@simplewebauthn/typescript-types@8.0.0:
resolution: {integrity: sha512-d7Izb2H+LZJteXMkS8DmpAarD6mZdpIOu/av/yH4/u/3Pd6DKFLyBM3j8BMmUvUqpzvJvHARNrRfQYto58mtTQ==}
dev: false
/@socket.io/component-emitter@3.1.0:
resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==}
dev: false

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="3pqdDc9GpRiC~~RXNJCBxa" x1="20.035" x2="4.818" y1="13.721" y2="29.585" gradientTransform="translate(0 14)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e5a505"/><stop offset=".01" stop-color="#e9a804"/><stop offset=".06" stop-color="#f4b102"/><stop offset=".129" stop-color="#fbb600"/><stop offset=".323" stop-color="#fdb700"/></linearGradient><path fill="url(#3pqdDc9GpRiC~~RXNJCBxa)" d="M12,41.5c0-1.381,1.119-2.5,2.5-2.5c0.156,0,0.307,0.019,0.454,0.046l1.186-1.186 C16.058,37.586,16,37.301,16,37c0-1.657,1.343-3,3-3c0.301,0,0.586,0.058,0.86,0.14L24,30l-6-6L4.586,37.414 C4.211,37.789,4,38.298,4,38.828v1.343c0,0.53,0.211,1.039,0.586,1.414l1.828,1.828C6.789,43.789,7.298,44,7.828,44h1.343 c0.53,0,1.039-0.211,1.414-0.586l1.46-1.46C12.019,41.807,12,41.656,12,41.5z"/><linearGradient id="3pqdDc9GpRiC~~RXNJCBxb" x1="21.64" x2="36.971" y1="-6.927" y2="15.362" gradientTransform="translate(0 14)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fede00"/><stop offset="1" stop-color="#ffd000"/></linearGradient><path fill="url(#3pqdDc9GpRiC~~RXNJCBxb)" d="M29.5,5C22.044,5,16,11.044,16,18.5S22.044,32,29.5,32S43,25.956,43,18.5S36.956,5,29.5,5z M33,19c-2.209,0-4-1.791-4-4s1.791-4,4-4s4,1.791,4,4S35.209,19,33,19z"/><path d="M39.86,27.16c-0.12,0.15-0.25,0.3-0.38,0.44c-2.47,2.7-6.03,4.4-9.98,4.4 h-0.11c-0.2,0-0.39-0.01-0.59-0.02c1.96-3,5.35-4.98,9.2-4.98C38.63,27,39.25,27.05,39.86,27.16z" opacity=".05"/><path d="M39.48,27.6c-2.47,2.7-6.03,4.4-9.98,4.4h-0.11 c1.9-2.72,5.05-4.5,8.61-4.5C38.5,27.5,39,27.54,39.48,27.6z" opacity=".07"/><linearGradient id="3pqdDc9GpRiC~~RXNJCBxc" x1="33.304" x2="42.696" y1="88.831" y2="71.169" gradientTransform="matrix(1 0 0 -1 0 118)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#21ad64"/><stop offset="1" stop-color="#088242"/></linearGradient><circle cx="38" cy="38" r="10" fill="url(#3pqdDc9GpRiC~~RXNJCBxc)"/><path fill="#fff" d="M38.5,43h-1c-0.276,0-0.5-0.224-0.5-0.5v-9c0-0.276,0.224-0.5,0.5-0.5h1c0.276,0,0.5,0.224,0.5,0.5v9 C39,42.776,38.776,43,38.5,43z"/><path fill="#fff" d="M33,38.5v-1c0-0.276,0.224-0.5,0.5-0.5h9c0.276,0,0.5,0.224,0.5,0.5v1c0,0.276-0.224,0.5-0.5,0.5h-9 C33.224,39,33,38.776,33,38.5z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#35c1f1" d="M21,23c-0.553,0-1,0.447-1,1v3c0,0.553,0.447,1,1,1s1-0.447,1-1v-3C22,23.447,21.553,23,21,23z"/><path fill="#35c1f1" d="M21,30c-0.553,0-1,0.447-1,1v12c0,0.553,0.447,1,1,1s1-0.447,1-1V31C22,30.447,21.553,30,21,30z"/><path fill="#35c1f1" d="M20.501,19.024C17.903,19.278,16,21.613,16,24.223V42c0,0.553,0.447,1,1,1s1-0.447,1-1V24.119 c0-1.451,0.977-2.784,2.402-3.061C22.317,20.686,24,22.15,24,24v18c0,0.553,0.447,1,1,1s1-0.447,1-1V24 C26,21.078,23.481,18.734,20.501,19.024z"/><path fill="#35c1f1" d="M21.252,15.003c-1.336-0.037-2.636,0.218-3.86,0.755c-0.506,0.221-0.737,0.811-0.515,1.317 c0.222,0.505,0.81,0.737,1.317,0.515c1.1-0.483,2.278-0.671,3.484-0.558C25.313,17.374,28,20.603,28,24.254V38c0,0.553,0.447,1,1,1 s1-0.447,1-1V24.345C30,19.414,26.182,15.139,21.252,15.003z"/><path fill="#35c1f1" d="M13.848,18.552C12.639,20.137,12,22.021,12,24v17c0,0.553,0.447,1,1,1s1-0.447,1-1V24 c0-1.538,0.497-3.002,1.437-4.235c0.335-0.439,0.251-1.066-0.188-1.401C14.81,18.027,14.182,18.114,13.848,18.552z"/><path fill="#35c1f1" d="M20.345,11.016C13.362,11.361,8,17.386,8,24.377V28c0,0.553,0.447,1,1,1s1-0.447,1-1v-3.677 c0-5.724,4.24-10.736,9.939-11.273C26.479,12.434,32,17.585,32,24v14.678c0,0.553,0.447,1,1,1s1-0.448,1-1V24 C34,16.615,27.809,10.648,20.345,11.016z"/><path fill="#35c1f1" d="M9,31c-0.553,0-1,0.447-1,1v5v1.678V39c0,0.552,0.448,1,1,1s1-0.448,1-1v-0.322V37v-5 C10,31.447,9.553,31,9,31z"/><path fill="#35c1f1" d="M30.958,11.458c-0.37,0.41-0.338,1.043,0.072,1.412C34.188,15.719,36,19.775,36,24v10 c0,0.553,0.447,1,1,1s1-0.447,1-1V24c0-4.79-2.052-9.388-5.63-12.614C31.96,11.016,31.328,11.048,30.958,11.458z"/><path fill="#35c1f1" d="M21,9c2.422,0,4.745,0.569,6.904,1.693c0.49,0.255,1.094,0.063,1.349-0.425 c0.255-0.491,0.064-1.094-0.425-1.349C26.38,7.646,23.747,7,21,7c-6.048,0-11.689,3.267-14.724,8.525 C6,16.004,6.164,16.615,6.643,16.891c0.157,0.091,0.329,0.134,0.499,0.134c0.345,0,0.681-0.179,0.867-0.5 C10.687,11.884,15.665,9,21,9z"/><path fill="#35c1f1" d="M5.841,18.824c-0.531-0.149-1.082,0.17-1.228,0.702C4.206,21.023,4,22.528,4,24v10 c0,0.553,0.447,1,1,1s1-0.447,1-1V24c0-1.294,0.183-2.623,0.543-3.948C6.688,19.518,6.373,18.969,5.841,18.824z"/><path fill="#35c1f1" d="M41.808,21.301c-0.07-0.548-0.565-0.927-1.119-0.865c-0.548,0.07-0.935,0.571-0.865,1.119 C39.944,22.488,40,23.265,40,24v3v2.45c0,0.552,0.448,1,1,1l0,0c0.552,0,1-0.448,1-1V27v-3C42,23.178,41.939,22.319,41.808,21.301z"/><path fill="#35c1f1" d="M5.695,11.122c-0.439-0.334-1.066-0.249-1.401,0.19C1.484,15.004,0,19.392,0,24v6 c0,0.553,0.447,1,1,1s1-0.447,1-1v-6c0-4.168,1.344-8.136,3.885-11.477C6.219,12.084,6.134,11.457,5.695,11.122z"/><path fill="#35c1f1" d="M40.22,18.432c0.521-0.179,0.799-0.749,0.619-1.271C37.919,8.691,29.946,3,21,3 C15.915,3,10.999,4.859,7.155,8.236C6.74,8.601,6.699,9.232,7.065,9.647c0.364,0.414,0.996,0.455,1.411,0.091 C11.953,6.683,16.401,5,21,5c8.093,0,15.306,5.149,17.949,12.813c0.142,0.414,0.53,0.674,0.945,0.674 C40.002,18.487,40.111,18.47,40.22,18.432z"/><path d="M28,33.41c0.5-1.09,1.18-2.09,2-2.96V38c0,0.55-0.45,1-1,1s-1-0.45-1-1 V33.41z" opacity=".05"/><path d="M32,28.78c0.62-0.41,1.29-0.75,2-1.03v8.93c0,0.55-0.45,1-1,1s-1-0.45-1-1 V28.78z" opacity=".05"/><path d="M38,27v5c0,0.55-0.45,1-1,1s-1-0.45-1-1v-4.82C36.65,27.06,37.32,27,38,27 z" opacity=".05"/><path d="M28,34.79c0.43-1.33,1.12-2.54,2-3.58V38c0,0.55-0.45,1-1,1s-1-0.45-1-1 V34.79z" opacity=".07"/><path d="M32,29.39c0.62-0.44,1.29-0.8,2-1.09v8.38c0,0.55-0.45,1-1,1s-1-0.45-1-1 V29.39z" opacity=".07"/><path d="M38,27.5V32c0,0.55-0.45,1-1,1s-1-0.45-1-1v-4.31 C36.65,27.57,37.32,27.5,38,27.5z" opacity=".07"/><path d="M42,27.75v1.7c0,0.55-0.45,1-1,1s-1-0.45-1-1v-2.27 C40.69,27.31,41.36,27.5,42,27.75z" opacity=".05"/><path d="M42,28.3v1.15c0,0.55-0.45,1-1,1s-1-0.45-1-1v-1.76 C40.69,27.83,41.37,28.03,42,28.3z" opacity=".07"/><linearGradient id="hns_QSPvAuABanH5z5S5qa" x1="33.304" x2="42.696" y1="90.831" y2="73.169" gradientTransform="matrix(1 0 0 -1 0 120)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#21ad64"/><stop offset="1" stop-color="#088242"/></linearGradient><circle cx="38" cy="38" r="10" fill="url(#hns_QSPvAuABanH5z5S5qa)"/><path fill="#fff" d="M38.5,43h-1c-0.276,0-0.5-0.224-0.5-0.5v-9c0-0.276,0.224-0.5,0.5-0.5h1c0.276,0,0.5,0.224,0.5,0.5v9 C39,42.776,38.776,43,38.5,43z"/><path fill="#fff" d="M33,38.5v-1c0-0.276,0.224-0.5,0.5-0.5h9c0.276,0,0.5,0.224,0.5,0.5v1c0,0.276-0.224,0.5-0.5,0.5h-9 C33.224,39,33,38.776,33,38.5z"/></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

@ -0,0 +1,83 @@
<template lang="pug">
q-dialog(ref='dialogRef', @hide='onDialogHide', persistent)
q-card(style='min-width: 650px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-add-key.svg', left, size='sm')
span {{t(`profile.passkeysAdd`)}}
.q-py-sm
.text-body2.q-px-md.q-py-sm {{t(`profile.passkeysNameHint`)}}
q-item
blueprint-icon(icon='key')
q-item-section
q-input(
outlined
v-model='state.name'
dense
hide-bottom-space
:label='t(`profile.passkeysName`)'
:aria-label='t(`profile.passkeysName`)'
autofocus
@keyup.enter='save'
)
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
flat
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='onDialogCancel'
)
q-btn(
unelevated
:label='t(`common.actions.save`)'
color='primary'
padding='xs md'
@click='save'
)
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive } from 'vue'
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
name: ''
})
// METHODS
async function save () {
try {
if (!state.name || state.name.trim().length < 1 || state.name.length > 255) {
throw new Error(t('profile.passkeysInvalidName'))
}
onDialogOK({
name: state.name
})
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
}
</script>

@ -1,68 +1,28 @@
<template lang="pug">
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 650px;')
q-dialog(ref='dialogRef', @hide='onDialogHide', persistent)
q-card.setup2fadialog(style='min-width: 450px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-password-reset.svg', left, size='sm')
span {{t(`admin.users.changePassword`)}}
q-form.q-py-sm(ref='changeUserPwdForm', @submit='save')
q-item
blueprint-icon(icon='lock')
q-item-section
q-input(
outlined
v-model='state.currentPassword'
dense
:rules='currentPasswordValidation'
hide-bottom-space
:label='t(`auth.changePwd.currentPassword`)'
:aria-label='t(`auth.changePwd.currentPassword`)'
lazy-rules='ondemand'
autofocus
)
q-item
blueprint-icon(icon='password')
q-item-section
q-input(
outlined
v-model='state.newPassword'
dense
:rules='newPasswordValidation'
hide-bottom-space
:label='t(`auth.changePwd.newPassword`)'
:aria-label='t(`auth.changePwd.newPassword`)'
lazy-rules='ondemand'
autofocus
)
template(#append)
.flex.items-center
q-badge(
:color='passwordStrength.color'
:label='passwordStrength.label'
)
q-separator.q-mx-sm(vertical)
q-btn(
flat
dense
padding='none xs'
color='brown'
@click='randomizePassword'
)
q-icon(name='las la-dice-d6')
.q-pl-xs.text-caption: strong Generate
q-item
blueprint-icon(icon='good-pincode')
q-item-section
q-input(
outlined
v-model='state.verifyPassword'
dense
:rules='verifyPasswordValidation'
hide-bottom-space
:label='t(`auth.changePwd.newPasswordVerify`)'
:aria-label='t(`auth.changePwd.newPasswordVerify`)'
lazy-rules='ondemand'
autofocus
q-icon(name='img:/_assets/icons/fluent-fingerprint.svg', left, size='sm')
span {{t(`profile.authSetTfa`)}}
template(v-if='!state.isInit')
q-linear-progress(query, color='positive')
q-card-section.text-center.text-grey {{t(`profile.authSetTfaLoading`)}}
template(v-else)
q-card-section.text-center
p {{t('auth.tfaSetupInstrFirst')}}
div(style='justify-content: center; display: flex;')
div(v-html='state.tfaQRImage', style='width: 200px;')
p.q-mt-sm {{t('auth.tfaSetupInstrSecond')}}
.flex.justify-center
v-otp-input(
v-model:value='state.securityCode'
:num-inputs='6'
:should-auto-focus='true'
input-classes='otp-input'
input-type='number'
separator=''
)
q-inner-loading(:showing='state.isLoading')
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
@ -74,7 +34,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
)
q-btn(
unelevated
:label='t(`common.actions.update`)'
:label='t(`auth.tfa.verifyToken`)'
color='primary'
padding='xs md'
@click='save'
@ -84,14 +44,14 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
<script setup>
import gql from 'graphql-tag'
import zxcvbn from 'zxcvbn'
import { sampleSize } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { computed, reactive, ref } from 'vue'
import { onMounted, reactive } from 'vue'
import { useSiteStore } from 'src/stores/site'
import VOtpInput from 'vue3-otp-input'
// PROPS
const props = defineProps({
@ -123,96 +83,78 @@ const { t } = useI18n()
// DATA
const state = reactive({
currentPassword: '',
newPassword: '',
verifyPassword: '',
isLoading: false
isInit: false,
isLoading: false,
securityCode: '',
tfaQRImage: '',
continuationToken: ''
})
// REFS
const changeUserPwdForm = ref(null)
// COMPUTED
// METHODS
const passwordStrength = computed(() => {
if (state.newPassword.length < 8) {
return {
color: 'negative',
label: t('admin.users.pwdStrengthWeak')
}
} else {
switch (zxcvbn(state.newPassword).score) {
case 1:
return {
color: 'deep-orange-7',
label: t('admin.users.pwdStrengthPoor')
}
case 2:
return {
color: 'purple-7',
label: t('admin.users.pwdStrengthMedium')
}
case 3:
return {
color: 'blue-7',
label: t('admin.users.pwdStrengthGood')
async function load () {
state.isInit = false
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation setupTfa (
$strategyId: UUID!
$siteId: UUID!
) {
setupTFA (
strategyId: $strategyId
siteId: $siteId
) {
operation {
succeeded
message
}
case 4:
return {
color: 'green-7',
label: t('admin.users.pwdStrengthStrong')
continuationToken
tfaQRImage
}
default:
return {
color: 'negative',
label: t('admin.users.pwdStrengthWeak')
}
`,
variables: {
strategyId: props.strategyId,
siteId: siteStore.id
}
})
if (resp?.data?.setupTFA?.operation?.succeeded) {
state.continuationToken = resp.data.setupTFA.continuationToken
state.tfaQRImage = resp.data.setupTFA.tfaQRImage
state.isInit = true
} else {
throw new Error(resp?.data?.setupTFA?.operation?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
// VALIDATION RULES
const currentPasswordValidation = [
val => val.length > 0 || t('auth.errors.missingPassword')
]
const newPasswordValidation = [
val => val.length > 0 || t('auth.errors.missingPassword'),
val => val.length >= 8 || t('auth.errors.passwordTooShort')
]
const verifyPasswordValidation = [
val => val.length > 0 || t('auth.errors.missingVerifyPassword'),
val => val === state.newPassword || t('auth.errors.passwordsNotMatch')
]
// METHODS
function randomizePassword () {
const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
state.newPassword = sampleSize(pwdChars, 16).join('')
onDialogCancel()
}
}
async function save () {
state.isLoading = true
try {
const isFormValid = await changeUserPwdForm.value.validate(true)
if (!isFormValid) {
throw new Error(t('auth.errors.fields'))
if (!/^[0-9]{6}$/.test(state.securityCode)) {
throw new Error(t('auth.errors.tfaMissing'))
}
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation changePwd (
$currentPassword: String
$newPassword: String!
mutation(
$continuationToken: String!
$securityCode: String!
$strategyId: UUID!
$siteId: UUID!
) {
changePassword (
currentPassword: $currentPassword
newPassword: $newPassword
loginTFA(
continuationToken: $continuationToken
securityCode: $securityCode
strategyId: $strategyId
siteId: $siteId
setup: true
) {
operation {
succeeded
@ -222,20 +164,23 @@ async function save () {
}
`,
variables: {
currentPassword: state.currentPassword,
newPassword: state.newPassword,
continuationToken: state.continuationToken,
securityCode: state.securityCode,
strategyId: props.strategyId,
siteId: siteStore.id
}
})
if (resp?.data?.changePassword?.operation?.succeeded) {
if (resp.data?.loginTFA?.operation?.succeeded) {
state.continuationToken = ''
state.securityCode = ''
$q.notify({
type: 'positive',
message: t('auth.changePwd.success')
message: t('auth.tfaSetupSuccess')
})
state.isLoading = false
onDialogOK()
} else {
throw new Error(resp?.data?.changePassword?.operation?.message || 'An unexpected error occured.')
throw new Error(resp.data?.loginTFA?.operation?.message || t('auth.errors.loginError'))
}
} catch (err) {
$q.notify({
@ -245,4 +190,46 @@ async function save () {
}
state.isLoading = false
}
onMounted(() => {
load()
})
</script>
<style lang="scss">
.setup2fadialog {
.otp-input {
width: 100%;
height: 48px;
padding: 5px;
margin: 0 5px 7px;
font-size: 20px;
border-radius: 6px;
text-align: center;
@at-root .body--light & {
border: 2px solid rgba(0, 0, 0, 0.2);
}
@at-root .body--dark & {
border: 2px solid rgba(255, 255, 255, 0.3);
}
&:focus-visible {
outline-color: $primary;
}
/* Background colour of an input field with value */
&.is-complete {
border-color: $positive;
border-width: 2px;
}
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
}
</style>

@ -0,0 +1,13 @@
/**
* Parse an error message for an error code and translate
*
* @param {String} val Value to parse
* @param {Function} t vue-i18n translation method
*/
export function localizeError (val, t) {
if (val?.startsWith('ERR_')) {
return t(`error.${val}`)
} else {
return val
}
}

@ -98,6 +98,7 @@ q-page.admin-locale
icon='las la-trash'
color='negative'
@click='deleteSite(site)'
:aria-label='t(`common.actions.delete`)'
)
</template>

@ -47,6 +47,46 @@ q-page.q-py-md(:style-fn='pageStyle')
@click='changePassword(auth.authId)'
)
.text-header.q-mt-md {{t('profile.passkeys')}}
.q-pa-md
.text-body2 {{ t('profile.passkeysIntro') }}
q-list.q-mt-lg(
v-if="state.passkeys?.length > 0"
bordered
separator
)
q-item(
v-for='pkey of state.passkeys'
:key='pkey.id'
)
q-item-section(avatar)
q-avatar(
color='secondary'
text-color='white'
rounded
)
q-icon(name='las la-key')
q-item-section
strong {{pkey.name}}
.text-caption {{ pkey.siteHostname }}
.text-caption.text-grey-7 {{ humanizeDate(pkey.createdAt) }}
q-item-section(side)
q-btn.acrylic-btn(
flat
icon='las la-trash'
:aria-label='t(`common.actions.delete`)'
color='negative'
@click='deactivatePasskey(pkey)'
)
.q-mt-md
q-btn(
icon='las la-plus'
unelevated
:label='t(`profile.passkeysAdd`)'
color='primary'
@click='setupPasskey'
)
q-inner-loading(:showing='state.loading > 0')
</template>
@ -55,11 +95,16 @@ import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { onMounted, reactive } from 'vue'
import { browserSupportsWebAuthn, startRegistration } from '@simplewebauthn/browser'
import { localizeError } from 'src/helpers/localization'
import { DateTime } from 'luxon'
import { useSiteStore } from 'src/stores/site'
import { useUserStore } from 'src/stores/user'
import ChangePwdDialog from 'src/components/ChangePwdDialog.vue'
import SetupTfaDialog from 'src/components/SetupTfaDialog.vue'
import PasskeyCreateDialog from 'src/components/PasskeyCreateDialog.vue'
// QUASAR
@ -67,6 +112,7 @@ const $q = useQuasar()
// STORES
const siteStore = useSiteStore()
const userStore = useUserStore()
// I18N
@ -83,6 +129,7 @@ useMeta({
const state = reactive({
authMethods: [],
passkeys: [],
loading: 0
})
@ -94,6 +141,10 @@ function pageStyle (offset, height) {
}
}
function humanizeDate (val) {
return DateTime.fromISO(val).toLocaleString(DateTime.DATETIME_MED)
}
async function fetchAuthMethods () {
state.loading++
try {
@ -113,6 +164,12 @@ async function fetchAuthMethods () {
strategyIcon
config
}
passkeys {
id
name
createdAt
siteHostname
}
}
}
`,
@ -122,6 +179,7 @@ async function fetchAuthMethods () {
fetchPolicy: 'network-only'
})
state.authMethods = respRaw.data?.userById?.auth ?? []
state.passkeys = respRaw.data?.userById?.passkeys ?? []
} catch (err) {
$q.notify({
type: 'negative',
@ -189,12 +247,166 @@ function disableTfa (strategyId) {
}
function setupTfa (strategyId) {
// $q.dialog({
// component: SetupTfaDialog,
// componentProps: {
// strategyId
// }
// })
$q.dialog({
component: SetupTfaDialog,
componentProps: {
strategyId
}
}).onOk(() => {
fetchAuthMethods()
})
}
async function setupPasskey () {
try {
if (!browserSupportsWebAuthn()) {
throw new Error(t('profile.passkeysUnsupported'))
}
$q.loading.show()
// -> Generation registration options
const genResp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation setupPasskey (
$siteId: UUID!
) {
setupPasskey(
siteId: $siteId
) {
operation {
succeeded
message
}
registrationOptions
}
}
`,
variables: {
siteId: siteStore.id
}
})
if (genResp?.data?.setupPasskey?.operation?.succeeded) {
state.registrationOptions = genResp.data.setupPasskey.registrationOptions
} else {
throw new Error(localizeError(genResp?.data?.setupPasskey?.operation?.message, t))
}
// -> Start registration on the authenticator
let attResp
try {
attResp = await startRegistration(state.registrationOptions)
} catch (err) {
if (err.name === 'InvalidStateError') {
throw new Error(t('error.ERR_PK_ALREADY_REGISTERED'))
} else {
throw err
}
}
// -> Prompt for passkey name
$q.loading.hide()
const passkeyName = await new Promise((resolve, reject) => {
$q.dialog({
component: PasskeyCreateDialog
}).onOk(({ name }) => {
resolve(name)
}).onCancel(() => {
reject(new Error(t('error.ERR_PK_USER_CANCELLED')))
})
})
$q.loading.show()
// -> Verify the authenticator response
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation finalizePasskey (
$registrationResponse: JSON!
$name: String!
) {
finalizePasskey(
registrationResponse: $registrationResponse
name: $name
) {
operation {
succeeded
message
}
}
}
`,
variables: {
registrationResponse: attResp,
name: passkeyName
}
})
if (resp?.data?.finalizePasskey?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('profile.passkeysSetupSuccess')
})
} else {
throw new Error(resp?.data?.finalizePasskey?.operation?.message)
}
} catch (err) {
$q.notify({
type: 'negative',
message: t('profile.passkeysSetupFailed'),
caption: err.message ?? 'An unexpected error occured.'
})
}
await fetchAuthMethods()
$q.loading.hide()
}
async function deactivatePasskey (pkey) {
$q.dialog({
title: t('common.actions.confirm'),
message: t('profile.passkeysDeactivateConfirm'),
cancel: true
}).onOk(async () => {
$q.loading.show()
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation deactivatePasskey (
$id: UUID!
) {
deactivatePasskey(
id: $id
) {
operation {
succeeded
message
}
}
}
`,
variables: {
id: pkey.id
}
})
if (resp?.data?.deactivatePasskey?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('profile.passkeysDeactivateSuccess')
})
} else {
throw new Error(resp?.data?.deactivatePasskey?.operation?.message)
}
} catch (err) {
$q.notify({
type: 'negative',
message: t('profile.passkeysDeactivateFailed'),
caption: err.message ?? 'An unexpected error occured.'
})
}
await fetchAuthMethods()
$q.loading.hide()
})
}
// MOUNTED

Loading…
Cancel
Save