diff --git a/server/db/migrations/3.0.0.mjs b/server/db/migrations/3.0.0.mjs index bd2ad60135..acef92f524 100644 --- a/server/db/migrations/3.0.0.mjs +++ b/server/db/migrations/3.0.0.mjs @@ -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) diff --git a/server/graph/resolvers/authentication.mjs b/server/graph/resolvers/authentication.mjs index 5d3c73f958..cf419b3910 100644 --- a/server/graph/resolvers/authentication.mjs +++ b/server/graph/resolvers/authentication.mjs @@ -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 */ diff --git a/server/graph/resolvers/user.mjs b/server/graph/resolvers/user.mjs index accb6dc967..9271af79bc 100644 --- a/server/graph/resolvers/user.mjs +++ b/server/graph/resolvers/user.mjs @@ -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) { diff --git a/server/graph/schemas/authentication.graphql b/server/graph/schemas/authentication.graphql index cc24a91e94..b7fdca4c7c 100644 --- a/server/graph/schemas/authentication.graphql +++ b/server/graph/schemas/authentication.graphql @@ -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! diff --git a/server/graph/schemas/user.graphql b/server/graph/schemas/user.graphql index 048f11db14..3473da3060 100644 --- a/server/graph/schemas/user.graphql +++ b/server/graph/schemas/user.graphql @@ -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 diff --git a/server/locales/en.json b/server/locales/en.json index aaf2a7722c..9120fe0e38 100644 --- a/server/locales/en.json +++ b/server/locales/en.json @@ -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.", diff --git a/server/models/users.mjs b/server/models/users.mjs index 95a7edade6..b24994f7b8 100644 --- a/server/models/users.mjs +++ b/server/models/users.mjs @@ -74,7 +74,7 @@ export class User extends Model { async generateTFA(strategyId, siteId) { WIKI.logger.debug(`Generating new TFA secret for user ${this.id}...`) - const site = WIKI.sites[siteId] ?? WIKI.sites[0] ?? { config: { title: 'Wiki' }} + const site = WIKI.sites[siteId] ?? WIKI.sites[0] ?? { config: { title: 'Wiki' } } const tfaInfo = tfa.generateSecret({ name: site.config.title, account: this.email @@ -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 diff --git a/server/modules/rendering/image-prefetch/renderer.mjs b/server/modules/rendering/image-prefetch/renderer.mjs index 07b8afa710..3e3eecd7be 100644 --- a/server/modules/rendering/image-prefetch/renderer.mjs +++ b/server/modules/rendering/image-prefetch/renderer.mjs @@ -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) } } diff --git a/server/package.json b/server/package.json index 09aed2444a..4b299a23c0 100644 --- a/server/package.json +++ b/server/package.json @@ -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": { diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index c6cfcec062..d8e14137ae 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -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'} diff --git a/ux/package.json b/ux/package.json index 4dfc900d5f..1a5422cb3d 100644 --- a/ux/package.json +++ b/ux/package.json @@ -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", diff --git a/ux/pnpm-lock.yaml b/ux/pnpm-lock.yaml index 211f3a7bf0..cf78399c1f 100644 --- a/ux/pnpm-lock.yaml +++ b/ux/pnpm-lock.yaml @@ -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 diff --git a/ux/public/_assets/icons/fluent-add-key.svg b/ux/public/_assets/icons/fluent-add-key.svg new file mode 100644 index 0000000000..9920976ade --- /dev/null +++ b/ux/public/_assets/icons/fluent-add-key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ux/public/_assets/icons/fluent-fingerprint.svg b/ux/public/_assets/icons/fluent-fingerprint.svg new file mode 100644 index 0000000000..b335cd746f --- /dev/null +++ b/ux/public/_assets/icons/fluent-fingerprint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ux/src/components/PasskeyCreateDialog.vue b/ux/src/components/PasskeyCreateDialog.vue new file mode 100644 index 0000000000..4bed60121f --- /dev/null +++ b/ux/src/components/PasskeyCreateDialog.vue @@ -0,0 +1,83 @@ + + + diff --git a/ux/src/components/SetupTfaDialog.vue b/ux/src/components/SetupTfaDialog.vue index 5fe108303d..129e54fca0 100644 --- a/ux/src/components/SetupTfaDialog.vue +++ b/ux/src/components/SetupTfaDialog.vue @@ -1,97 +1,57 @@ + + diff --git a/ux/src/helpers/localization.js b/ux/src/helpers/localization.js new file mode 100644 index 0000000000..8f127d5142 --- /dev/null +++ b/ux/src/helpers/localization.js @@ -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 + } +} diff --git a/ux/src/pages/AdminSites.vue b/ux/src/pages/AdminSites.vue index 6cb4fd478a..4b2ec80861 100644 --- a/ux/src/pages/AdminSites.vue +++ b/ux/src/pages/AdminSites.vue @@ -98,6 +98,7 @@ q-page.admin-locale icon='las la-trash' color='negative' @click='deleteSite(site)' + :aria-label='t(`common.actions.delete`)' ) diff --git a/ux/src/pages/ProfileAuth.vue b/ux/src/pages/ProfileAuth.vue index 5ff02a6930..2b59584cfd 100644 --- a/ux/src/pages/ProfileAuth.vue +++ b/ux/src/pages/ProfileAuth.vue @@ -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') @@ -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