- added api UserController.ts for 2FA

- added PersonalTotpSettings.vue vor enablin/disabling 2FA
- changed User.ts: added attributes: state, twoFactorSecret and twoFactorRecoveryCodes
- added resources/js/utils/toast.ts for notifications
- modified start/routes/api.ts
- npm updates
This commit is contained in:
Kaimbacher 2024-01-19 15:33:46 +01:00
parent 18635f77b3
commit ebc62d9117
18 changed files with 1151 additions and 315 deletions

View File

@ -0,0 +1,82 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
// import TotpSecret from 'App/Models/TotpSecret';
import User from 'App/Models/User';
import TwoFactorAuthProvider from 'App/Services/TwoFactorAuthProvider';
import { StatusCodes } from 'http-status-codes';
import { InvalidArgumentException } from 'node-exceptions';
import { TotpState } from 'Contracts/enums';
// Here we are generating secret and recovery codes for the user thats enabling 2FA and storing them to our database.
export default class UserController {
public async enable({ auth, response, request }: HttpContextContract) {
const user = (await User.find(auth.user?.id)) as User;
// await user.load('totp_secret');
// if (!user.totp_secret) {
// let totpSecret = new TotpSecret();
// user.related('totp_secret').save(totpSecret);
// await user.load('totp_secret');
// }
if (!user) {
throw new Error('user not available');
}
const state: number = request.input('state');
try {
switch (state) {
case TotpState.STATE_DISABLED:
// user.twoFactorSecret = null;
// user.twoFactorRecoveryCodes = null;
user.twoFactorSecret = "";
user.twoFactorRecoveryCodes = [""];
await user.save();
user.state = TotpState.STATE_DISABLED;
await user.save();
return response.status(StatusCodes.OK).json({
state: TotpState.STATE_DISABLED,
});
case TotpState.STATE_CREATED:
user.twoFactorSecret = TwoFactorAuthProvider.generateSecret(user);
user.state = TotpState.STATE_CREATED;
await user.save();
let qrcode = await TwoFactorAuthProvider.generateQrCode(user);
// throw new InvalidArgumentException('code is missing');
return response.status(StatusCodes.OK).json({
state: user.state,
secret: user.twoFactorSecret,
url: qrcode.url,
svg: qrcode.svg,
});
case TotpState.STATE_ENABLED:
let code: string = request.input('code');
if (!code) {
throw new InvalidArgumentException('code is missing');
}
const success = await TwoFactorAuthProvider.enable(user, code)
return response.status(StatusCodes.OK).json({
state: success ? TotpState.STATE_ENABLED : TotpState.STATE_CREATED,
});
default:
throw new InvalidArgumentException('Invalid TOTP state');
}
} catch (error) {
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: 'Invalid TOTP state',
});
}
}
// public async fetchRecoveryCodes({ auth, view }) {
// const user = auth?.user;
// return view.render('pages/settings', {
// twoFactorEnabled: user.isTwoFactorEnabled,
// recoveryCodes: user.twoFactorRecoveryCodes,
// });
// }
}

View File

@ -22,7 +22,7 @@ export default class UserController {
return inertia.render('Auth/AccountInfo', {
user: user,
twoFactorEnabled: user.isTwoFactorEnabled,
code: await TwoFactorAuthProvider.generateQrCode(user),
// code: await TwoFactorAuthProvider.generateQrCode(user),
});
}

View File

@ -5,7 +5,7 @@ import type { ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Orm';
import Field from 'App/Library/Field';
import BaseModel from 'App/Models/BaseModel';
import { DateTime } from 'luxon';
import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator';
import { schema, rules } from '@ioc:Adonis/Core/Validator';
export default class DatasetsController {
public async index({ auth, request, inertia }: HttpContextContract) {

63
app/Models/TotpSecret.ts Normal file
View File

@ -0,0 +1,63 @@
import { column, BaseModel, SnakeCaseNamingStrategy, belongsTo, BelongsTo } from '@ioc:Adonis/Lucid/Orm';
import User from './User';
import { DateTime } from 'luxon';
import dayjs from 'dayjs';
import Encryption from '@ioc:Adonis/Core/Encryption';
export default class TotpSecret extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy();
public static table = 'totp_secrets';
// public static fillable: string[] = ['value', 'label', 'type', 'relation'];
@column({
isPrimary: true,
})
public id: number;
@column({})
public user_id: number;
// @column()
// public twoFactorSecret: string;
@column({
serializeAs: null,
consume: (value: string) => (value ? JSON.parse(Encryption.decrypt(value) ?? '{}') : null),
prepare: (value: string) => Encryption.encrypt(JSON.stringify(value)),
})
public twoFactorSecret?: string | null;
// serializeAs: null removes the model properties from the serialized output.
@column({
serializeAs: null,
consume: (value: string) => (value ? JSON.parse(Encryption.decrypt(value) ?? '[]') : []),
prepare: (value: string[]) => Encryption.encrypt(JSON.stringify(value)),
})
public twoFactorRecoveryCodes?: string[] | null;
@column({})
public state: number;
@column.dateTime({
serialize: (value: Date | null) => {
// return value ? moment(value).format('MMMM Do YYYY, HH:mm:ss') : value;
return value ? dayjs(value).format('MMMM D YYYY HH:mm a') : value;
},
autoCreate: true,
})
public created_at: DateTime;
@column.dateTime({
serialize: (value: Date | null) => {
return value ? dayjs(value).format('MMMM D YYYY HH:mm a') : value;
},
autoCreate: true,
autoUpdate: true,
})
public updated_at: DateTime;
@belongsTo(() => User, {
foreignKey: 'user_id',
})
public user: BelongsTo<typeof User>;
}

View File

@ -7,6 +7,8 @@ import Config from '@ioc:Adonis/Core/Config';
import Dataset from './Dataset';
import BaseModel from './BaseModel';
import Encryption from '@ioc:Adonis/Core/Encryption';
import { TotpState } from 'Contracts/enums';
// import TotpSecret from './TotpSecret';
// export default interface IUser {
// id: number;
@ -51,7 +53,7 @@ export default class User extends BaseModel {
consume: (value: string) => (value ? JSON.parse(Encryption.decrypt(value) ?? '{}') : null),
prepare: (value: string) => Encryption.encrypt(JSON.stringify(value)),
})
public twoFactorSecret?: string;
public twoFactorSecret?: string | null;
// serializeAs: null removes the model properties from the serialized output.
@column({
@ -59,7 +61,15 @@ export default class User extends BaseModel {
consume: (value: string) => (value ? JSON.parse(Encryption.decrypt(value) ?? '[]') : []),
prepare: (value: string[]) => Encryption.encrypt(JSON.stringify(value)),
})
public twoFactorRecoveryCodes?: string[];
public twoFactorRecoveryCodes?: string[] | null;
@column({})
public state: number;
// @hasOne(() => TotpSecret, {
// foreignKey: 'user_id',
// })
// public totp_secret: HasOne<typeof TotpSecret>;
@beforeSave()
public static async hashPassword(user) {
@ -68,8 +78,9 @@ export default class User extends BaseModel {
}
}
public get isTwoFactorEnabled() {
return Boolean(this?.twoFactorSecret);
public get isTwoFactorEnabled(): boolean {
return Boolean(this?.twoFactorSecret && this.state == TotpState.STATE_ENABLED);
// return Boolean(this.totp_secret?.twoFactorSecret);
}
@manyToMany(() => Role, {

View File

@ -1,9 +1,10 @@
import Config from '@ioc:Adonis/Core/Config';
import User from 'App/Models/User';
import { generateSecret } from 'node-2fa/dist/index';
import { generateSecret, verifyToken } from 'node-2fa/dist/index';
// import cryptoRandomString from 'crypto-random-string';
import QRCode from 'qrcode';
import crypto from 'crypto';
import { TotpState } from 'Contracts/enums';
// npm install node-2fa --save
// npm install crypto-random-string --save
@ -73,16 +74,44 @@ class TwoFactorAuthProvider {
}
}
public async generateQrCode(user: User) : Promise<{svg: string; url: string; }> {
// public async generateQrCode(user: User) : Promise<{svg: string; url: string; secret: string; }> {
// const issuer = encodeURIComponent(this.issuer); // 'TethysCloud'
// // const userName = encodeURIComponent(user.email); // 'rrqx9472%40tethys.at'
// const label = `${this.issuer}:${user.email}`;
// const algorithm = encodeURIComponent("SHA256");
// const query = `?secret=${user.twoFactorSecret}&issuer=${issuer}&algorithm=${algorithm}&digits=6`; // '?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud'
// const url = `otpauth://totp/${label}${query}`; // 'otpauth://totp/rrqx9472%40tethys.at?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud'
// const svg = await QRCode.toDataURL(url);
// const secret = user.twoFactorSecret as string;
// return { svg, url, secret };
// }
public async generateQrCode(user: User, twoFactorSecret?: string): Promise<{ svg: string; url: string; secret: string }> {
const issuer = encodeURIComponent(this.issuer); // 'TethysCloud'
// const userName = encodeURIComponent(user.email); // 'rrqx9472%40tethys.at'
const label = `${this.issuer}:${user.email}`;
const algorithm = encodeURIComponent("SHA256");
const query = `?secret=${user.twoFactorSecret}&issuer=${issuer}&algorithm=${algorithm}&digits=6`; // '?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud'
// const algorithm = encodeURIComponent('SHA256');
const secret = twoFactorSecret ? twoFactorSecret : (user.twoFactorSecret as string);
const query = `?secret=${secret}&issuer=${issuer}&digits=6`; // '?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud'
const url = `otpauth://totp/${label}${query}`; // 'otpauth://totp/rrqx9472%40tethys.at?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud'
const svg = await QRCode.toDataURL(url);
return { svg, url };
return { svg, url, secret };
}
public async enable(user: User, token: string): Promise<boolean> {
const isValid = verifyToken(user.twoFactorSecret as string, token, 1);
if (!isValid) {
return false;
}
user.state = TotpState.STATE_ENABLED;
if (await user.save()) {
return true;
}
return false;
}
}

View File

@ -111,3 +111,9 @@ export enum IdentifierTypes {
url = 'url',
urn = 'urn',
}
export enum TotpState {
STATE_DISABLED = 0,
STATE_CREATED = 1,
STATE_ENABLED = 2,
}

View File

@ -16,6 +16,8 @@ export default class Accounts extends BaseSchema {
table.timestamp('updated_at');
table.text("two_factor_secret").nullable();
table.text("two_factor_recovery_codes").nullable();
table.smallint('state').nullable();
table.bigint('last_counter').nullable();
});
}
@ -39,8 +41,47 @@ export default class Accounts extends BaseSchema {
// CONSTRAINT accounts_email_unique UNIQUE (email)
// two_factor_secret text COLLATE pg_catalog."default",
// two_factor_recovery_codes text COLLATE pg_catalog."default",
// state smallint,
// last_counter bigint,
// )
// ALTER TABLE gba.accounts
// ADD COLUMN two_factor_secret text COLLATE pg_catalog."default",
// ADD COLUMN two_factor_recovery_codes text COLLATE pg_catalog."default";
// ADD COLUMN two_factor_recovery_codes text COLLATE pg_catalog."default",
// ADD COLUMN state smallint,
// ADD COLUMN last_counter bigint;
// CREATE TABLE IF NOT EXISTS gba.totp_secrets
// (
// id integer NOT NULL,
// user_id integer NOT NULL,
// state smallint,
// last_counter bigint,
// two_factor_secret text COLLATE pg_catalog."default",
// two_factor_recovery_codes text COLLATE pg_catalog."default",
// created_at timestamp(0) without time zone,
// updated_at timestamp(0) without time zone,
// CONSTRAINT totp_secrets_pkey PRIMARY KEY (id),
// CONSTRAINT totp_secrets_user_id_foreign FOREIGN KEY (user_id)
// REFERENCES gba.accounts (id) MATCH SIMPLE
// ON UPDATE CASCADE
// ON DELETE CASCADE
// );
// CREATE SEQUENCE IF NOT EXISTS gba.totp_secrets_id_seq
// INCREMENT 1
// START 1
// MINVALUE 1
// MAXVALUE 2147483647
// CACHE 1
// OWNED BY gba.totp_secrets.id;
// ALTER SEQUENCE gba.totp_secrets_id_seq
// OWNER TO tethys_admin;
// GRANT ALL ON SEQUENCE gba.totp_secrets_id_seq TO tethys_admin;
// ALTER TABLE gba.totp_secrets ALTER COLUMN id SET DEFAULT nextval('gba.totp_secrets_id_seq');

437
package-lock.json generated
View File

@ -32,6 +32,7 @@
"leaflet": "^1.9.3",
"luxon": "^3.2.1",
"node-2fa": "^2.0.3",
"node-exceptions": "^4.0.1",
"notiwind": "^2.0.0",
"pg": "^8.9.0",
"proxy-addr": "^2.0.7",
@ -40,6 +41,7 @@
"reflect-metadata": "^0.2.1",
"saxon-js": "^2.5.0",
"source-map-support": "^0.5.21",
"toastify-js": "^1.12.0",
"vuedraggable": "^4.1.0",
"xmlbuilder2": "^3.1.1"
},
@ -972,9 +974,9 @@
}
},
"node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz",
"integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz",
"integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==",
"dev": true,
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
@ -1197,9 +1199,9 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.7.tgz",
"integrity": "sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==",
"version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz",
"integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==",
"dev": true,
"dependencies": {
"@babel/template": "^7.22.15",
@ -1723,16 +1725,15 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz",
"integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==",
"version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz",
"integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.15",
"@babel/helper-compilation-targets": "^7.23.6",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-optimise-call-expression": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-replace-supers": "^7.22.20",
"@babel/helper-split-export-declaration": "^7.22.6",
@ -2430,9 +2431,9 @@
}
},
"node_modules/@babel/preset-env": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.7.tgz",
"integrity": "sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==",
"version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.8.tgz",
"integrity": "sha512-lFlpmkApLkEP6woIKprO6DO60RImpatTQKtz4sUcDjVcK8M8mQ4sZsuxaTMNOZf0sqAq/ReYW1ZBHnOQwKpLWA==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.23.5",
@ -2468,7 +2469,7 @@
"@babel/plugin-transform-block-scoping": "^7.23.4",
"@babel/plugin-transform-class-properties": "^7.23.3",
"@babel/plugin-transform-class-static-block": "^7.23.4",
"@babel/plugin-transform-classes": "^7.23.5",
"@babel/plugin-transform-classes": "^7.23.8",
"@babel/plugin-transform-computed-properties": "^7.23.3",
"@babel/plugin-transform-destructuring": "^7.23.3",
"@babel/plugin-transform-dotall-regex": "^7.23.3",
@ -2572,9 +2573,9 @@
"dev": true
},
"node_modules/@babel/runtime": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz",
"integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==",
"version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz",
"integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
@ -2827,13 +2828,13 @@
"integrity": "sha512-qF0aH5UiZvCmneX5orJbVRoc2VTyLTV3X/7laMp03Qt28L+B9tFlZODOGUL64wDWc69YVdi1LeJB0cIgd51lvw=="
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.1",
"debug": "^4.1.1",
"@humanwhocodes/object-schema": "^2.0.2",
"debug": "^4.3.1",
"minimatch": "^3.0.5"
},
"engines": {
@ -2854,9 +2855,9 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true
},
"node_modules/@inertiajs/core": {
@ -2871,9 +2872,9 @@
}
},
"node_modules/@inertiajs/core/node_modules/axios": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.4.tgz",
"integrity": "sha512-heJnIs6N4aa1eSthhN9M5ioILu8Wi8vmQW9iHQ9NUvfkJb0lEEDUiIdQNAuBtfUt3FxReaKdpQA5DbmMOqzF/A==",
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"dependencies": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
@ -3303,9 +3304,9 @@
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"version": "0.3.21",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz",
"integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@ -3460,9 +3461,9 @@
}
},
"node_modules/@opensearch-project/opensearch": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.4.0.tgz",
"integrity": "sha512-r0ZNIlDxAua1ZecOBJ8qOXshf2ZQhNKmfly7o0aNuACf0pDa6Et/8mWMZuaFOu7xlNEeRNB7IjDQUYFy2SPElw==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.5.0.tgz",
"integrity": "sha512-RY5J6Jt/Jbbr2F9XByGY9LJr0VNmXJjgVvvntpKE4NtZa/r9ak3o8YtGK1iey1yHgzMzze25598qq7ZYFk42DA==",
"dependencies": {
"aws4": "^1.11.0",
"debug": "^4.3.1",
@ -3494,9 +3495,9 @@
}
},
"node_modules/@pkgr/core": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz",
"integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==",
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
"integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
@ -4039,9 +4040,9 @@
"dev": true
},
"node_modules/@types/eslint": {
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.1.tgz",
"integrity": "sha512-18PLWRzhy9glDQp3+wOgfLYRWlhgX0azxgJ63rdpoUHyrC9z0f5CkFburjQx4uD7ZCruw85ZtMt6K+L+R8fLJQ==",
"version": "8.56.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz",
"integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==",
"dev": true,
"peer": true,
"dependencies": {
@ -4205,9 +4206,9 @@
}
},
"node_modules/@types/luxon": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.8.tgz",
"integrity": "sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ=="
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.1.tgz",
"integrity": "sha512-m1KQEZZCITtheRhMVq5jDvAl0HwFhunLs7x6tpFFvUTJpKfmewS/Ymg+YA97/s8w1I1nC4pJyi0aAnn+vf3yew=="
},
"node_modules/@types/md5": {
"version": "2.3.5",
@ -4227,9 +4228,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.10.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz",
"integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==",
"version": "20.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
"integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==",
"dependencies": {
"undici-types": "~5.26.4"
}
@ -4381,9 +4382,9 @@
}
},
"node_modules/@types/validator": {
"version": "13.11.7",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.7.tgz",
"integrity": "sha512-q0JomTsJ2I5Mv7dhHhQLGjMvX0JJm5dyZ1DXQySIUzU1UlwzB8bt+R6+LODUbz0UDIOvEzGc28tk27gBJw2N8Q=="
"version": "13.11.8",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz",
"integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ=="
},
"node_modules/@types/ws": {
"version": "8.5.10",
@ -4676,49 +4677,49 @@
"dev": true
},
"node_modules/@vue/compiler-core": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.4.tgz",
"integrity": "sha512-U5AdCN+6skzh2bSJrkMj2KZsVkUpgK8/XlxjSRYQZhNPcvt9/kmgIMpFEiTyK+Dz5E1J+8o8//BEIX+bakgVSw==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
"integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/shared": "3.4.4",
"@vue/shared": "3.4.15",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.4.tgz",
"integrity": "sha512-iSwkdDULCN+Vr8z6uwdlL044GJ/nUmECxP9vu7MzEs4Qma0FwDLYvnvRcyO0ZITuu3Os4FptGUDnhi1kOLSaGw==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
"integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
"dependencies": {
"@vue/compiler-core": "3.4.4",
"@vue/shared": "3.4.4"
"@vue/compiler-core": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.4.tgz",
"integrity": "sha512-OTFcU6vUxUNHBcarzkp4g6d25nvcmDvFDzPRvSrIsByFFPRYN+y3b+j9HxYwt6nlWvGyFCe0roeJdJlfYxbCBg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
"integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/compiler-core": "3.4.4",
"@vue/compiler-dom": "3.4.4",
"@vue/compiler-ssr": "3.4.4",
"@vue/shared": "3.4.4",
"@vue/compiler-core": "3.4.15",
"@vue/compiler-dom": "3.4.15",
"@vue/compiler-ssr": "3.4.15",
"@vue/shared": "3.4.15",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5",
"postcss": "^8.4.32",
"postcss": "^8.4.33",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.4.tgz",
"integrity": "sha512-1DU9DflSSQlx/M61GEBN+NbT/anUki2ooDo9IXfTckCeKA/2IKNhY8KbG3x6zkd3KGrxzteC7de6QL88vEb41Q==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
"integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
"dependencies": {
"@vue/compiler-dom": "3.4.4",
"@vue/shared": "3.4.4"
"@vue/compiler-dom": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/devtools-api": {
@ -4728,53 +4729,48 @@
"dev": true
},
"node_modules/@vue/reactivity": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.4.tgz",
"integrity": "sha512-DFsuJBf6sfhd5SYzJmcBTUG9+EKqjF31Gsk1NJtnpJm9liSZ806XwGJUeNBVQIanax7ODV7Lmk/k17BgxXNuTg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
"integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
"dependencies": {
"@vue/shared": "3.4.4"
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.4.tgz",
"integrity": "sha512-zWWwNQAj5JdxrmOA1xegJm+c4VtyIbDEKgQjSb4va5v7gGTCh0ZjvLI+htGFdVXaO9bs2J3C81p5p+6jrPK8Bw==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
"integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
"dependencies": {
"@vue/reactivity": "3.4.4",
"@vue/shared": "3.4.4"
"@vue/reactivity": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.4.tgz",
"integrity": "sha512-Nlh2ap1J/eJQ6R0g+AIRyGNwpTJQACN0dk8I8FRLH8Ev11DSvfcPOpn4+Kbg5xAMcuq0cHB8zFYxVrOgETrrvg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
"integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
"dependencies": {
"@vue/runtime-core": "3.4.4",
"@vue/shared": "3.4.4",
"@vue/runtime-core": "3.4.15",
"@vue/shared": "3.4.15",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/runtime-dom/node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/@vue/server-renderer": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.4.tgz",
"integrity": "sha512-+AjoiKcC41k7SMJBYkDO9xs79/Of8DiThS9mH5l2MK+EY0to3psI0k+sElvVqQvsoZTjHMEuMz0AEgvm2T+CwA==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
"integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
"dependencies": {
"@vue/compiler-ssr": "3.4.4",
"@vue/shared": "3.4.4"
"@vue/compiler-ssr": "3.4.15",
"@vue/shared": "3.4.15"
},
"peerDependencies": {
"vue": "3.4.4"
"vue": "3.4.15"
}
},
"node_modules/@vue/shared": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.4.tgz",
"integrity": "sha512-abSgiVRhfjfl3JALR/cSuBl74hGJ3SePgf1mKzodf1eMWLwHZbfEGxT2cNJSsNiw44jEgrO7bNkhchaWA7RwNw=="
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g=="
},
"node_modules/@vue/tsconfig": {
"version": "0.4.0",
@ -5116,9 +5112,9 @@
}
},
"node_modules/acorn-walk": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz",
"integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==",
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
"engines": {
"node": ">=0.4.0"
}
@ -5651,9 +5647,9 @@
}
},
"node_modules/autoprefixer": {
"version": "10.4.16",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
"integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
"version": "10.4.17",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
"integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
"dev": true,
"funding": [
{
@ -5670,9 +5666,9 @@
}
],
"dependencies": {
"browserslist": "^4.21.10",
"caniuse-lite": "^1.0.30001538",
"fraction.js": "^4.3.6",
"browserslist": "^4.22.2",
"caniuse-lite": "^1.0.30001578",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
@ -5912,13 +5908,13 @@
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz",
"integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==",
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz",
"integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.22.6",
"@babel/helper-define-polyfill-provider": "^0.4.4",
"@babel/helper-define-polyfill-provider": "^0.5.0",
"semver": "^6.3.1"
},
"peerDependencies": {
@ -5947,13 +5943,29 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/babel-plugin-polyfill-regenerator": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz",
"integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==",
"node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz",
"integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==",
"dev": true,
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.4.4"
"@babel/helper-compilation-targets": "^7.22.6",
"@babel/helper-plugin-utils": "^7.22.5",
"debug": "^4.1.1",
"lodash.debounce": "^4.0.8",
"resolve": "^1.14.2"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/babel-plugin-polyfill-regenerator": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz",
"integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==",
"dev": true,
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.5.0"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@ -6131,9 +6143,9 @@
}
},
"node_modules/bonjour-service": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.0.tgz",
"integrity": "sha512-xdzMA6JGckxyJzZByjEWRcfKmDxXaGXZWVftah3FkCqdlePNS9DjHSUN5zkP4oEfz/t0EXXlro88EIhzwMB4zA==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz",
"integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
@ -6372,9 +6384,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001574",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001574.tgz",
"integrity": "sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg==",
"version": "1.0.30001579",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz",
"integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==",
"dev": true,
"funding": [
{
@ -6415,9 +6427,9 @@
}
},
"node_modules/chai": {
"version": "4.3.10",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
"integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
"integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==",
"dev": true,
"dependencies": {
"assertion-error": "^1.1.0",
@ -7179,19 +7191,19 @@
}
},
"node_modules/css-loader": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz",
"integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.9.1.tgz",
"integrity": "sha512-OzABOh0+26JKFdMzlK6PY1u5Zx8+Ck7CVRlcGNZoY9qwJjdfu2VWFuprTIpPW+Av5TZTVViYWcFQaEEQURLknQ==",
"dev": true,
"dependencies": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.21",
"postcss": "^8.4.33",
"postcss-modules-extract-imports": "^3.0.0",
"postcss-modules-local-by-default": "^4.0.3",
"postcss-modules-scope": "^3.0.0",
"postcss-modules-local-by-default": "^4.0.4",
"postcss-modules-scope": "^3.1.1",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
"semver": "^7.3.8"
"semver": "^7.5.4"
},
"engines": {
"node": ">= 12.13.0"
@ -7297,6 +7309,12 @@
"csstype": "~3.0.5"
}
},
"node_modules/css-render/node_modules/csstype": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
"dev": true
},
"node_modules/css-select": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
@ -7459,10 +7477,9 @@
"dev": true
},
"node_modules/csstype": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
"dev": true
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/cuid": {
"version": "2.1.8",
@ -8111,9 +8128,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.620",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.620.tgz",
"integrity": "sha512-a2fcSHOHrqBJsPNXtf6ZCEZpXrFCcbK1FBxfX3txoqWzNgtEDG1f3M59M98iwxhRW4iMKESnSjbJ310/rkrp0g==",
"version": "1.4.639",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.639.tgz",
"integrity": "sha512-CkKf3ZUVZchr+zDpAlNLEEy2NJJ9T64ULWaDgy3THXXlPVPkLu3VOs9Bac44nebVtdwl2geSj6AxTtGDOxoXhg==",
"dev": true
},
"node_modules/emittery": {
@ -8352,9 +8369,9 @@
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.2.tgz",
"integrity": "sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
"integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
"dev": true,
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
@ -9164,9 +9181,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
@ -11849,9 +11866,9 @@
}
},
"node_modules/mini-css-extract-plugin": {
"version": "2.7.6",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz",
"integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==",
"version": "2.7.7",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.7.tgz",
"integrity": "sha512-+0n11YGyRavUR3IlaOzJ0/4Il1avMvJ1VJfhWfCn24ITQXhRr1gghbhhrda6tgtNcpZaWKdSuwKq20Jb7fnlyw==",
"dev": true,
"dependencies": {
"schema-utils": "^4.0.0"
@ -12100,9 +12117,9 @@
}
},
"node_modules/naive-ui": {
"version": "2.36.0",
"resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.36.0.tgz",
"integrity": "sha512-r1ydtEm1Ryf/aWpbLCf32mQAGK99jd1eXgpkCtIomcBRZeAtusfy6zCtIpCppoCuIKM3BW5DMafhVxilubk/lQ==",
"version": "2.37.3",
"resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.37.3.tgz",
"integrity": "sha512-aUkHFXVIluSi8Me+npbcsdv1NYhVMj5t9YaruoCESlqmfqspj+R2QHEVXkTtUI1kQwVrABMCtAGq/wountqjZA==",
"dev": true,
"dependencies": {
"@css-render/plugin-bem": "^0.15.12",
@ -12112,6 +12129,7 @@
"@types/lodash-es": "^4.17.9",
"async-validator": "^4.2.5",
"css-render": "^0.15.12",
"csstype": "^3.1.3",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"evtd": "^0.2.4",
@ -12122,7 +12140,7 @@
"treemate": "^0.3.11",
"vdirs": "^0.1.8",
"vooks": "^0.2.12",
"vueuc": "^0.4.54"
"vueuc": "^0.4.58"
},
"peerDependencies": {
"vue": "^3.0.0"
@ -12229,6 +12247,11 @@
"lodash": "^4.17.21"
}
},
"node_modules/node-exceptions": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/node-exceptions/-/node-exceptions-4.0.1.tgz",
"integrity": "sha512-KJI+FawYOv74x60H6+zrBPfO2vvp9m0pHZi6SH8BBBuc67Irv11DsqY4Le4EBFq1/T5aXFU3hkLrMgtW7RNXxA=="
},
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@ -13160,9 +13183,9 @@
}
},
"node_modules/pino-pretty/node_modules/sonic-boom": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz",
"integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz",
"integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
@ -13358,9 +13381,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
"version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
"integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
"funding": [
{
"type": "opencollective",
@ -13686,9 +13709,9 @@
}
},
"node_modules/postcss-modules-local-by-default": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz",
"integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz",
"integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==",
"dev": true,
"dependencies": {
"icss-utils": "^5.0.0",
@ -13703,9 +13726,9 @@
}
},
"node_modules/postcss-modules-scope": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.0.tgz",
"integrity": "sha512-SaIbK8XW+MZbd0xHPf7kdfA/3eOt7vxJ72IRecn3EzuZVLr1r0orzf0MX/pN8m+NMDoo6X/SQd8oeKqGZd8PXg==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz",
"integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==",
"dev": true,
"dependencies": {
"postcss-selector-parser": "^6.0.4"
@ -14026,9 +14049,9 @@
}
},
"node_modules/prettier": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
"integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
@ -14789,9 +14812,9 @@
}
},
"node_modules/saxon-js/node_modules/axios": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.4.tgz",
"integrity": "sha512-heJnIs6N4aa1eSthhN9M5ioILu8Wi8vmQW9iHQ9NUvfkJb0lEEDUiIdQNAuBtfUt3FxReaKdpQA5DbmMOqzF/A==",
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"dependencies": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
@ -14958,9 +14981,9 @@
}
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"dependencies": {
"randombytes": "^2.1.0"
@ -15070,14 +15093,15 @@
"dev": true
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
"integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
"dependencies": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.2",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
"has-property-descriptors": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
@ -15752,9 +15776,9 @@
}
},
"node_modules/style-loader": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz",
"integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
"integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==",
"dev": true,
"engines": {
"node": ">= 12.13.0"
@ -15894,13 +15918,13 @@
}
},
"node_modules/supertest": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz",
"integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==",
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz",
"integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==",
"dev": true,
"dependencies": {
"methods": "^1.1.2",
"superagent": "^8.0.5"
"superagent": "^8.1.2"
},
"engines": {
"node": ">=6.4.0"
@ -16138,9 +16162,9 @@
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
},
"node_modules/tailwindcss": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
"integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
"integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
"dev": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@ -16212,9 +16236,9 @@
}
},
"node_modules/terser": {
"version": "5.26.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz",
"integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==",
"version": "5.27.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
"integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@ -16446,6 +16470,11 @@
"node": ">=8.0"
}
},
"node_modules/toastify-js": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
"integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ=="
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -16997,15 +17026,15 @@
}
},
"node_modules/vue": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.4.tgz",
"integrity": "sha512-suZXgDVT8lRNhKmxdkwOsR0oyUi8is7mtqI18qW97JLoyorEbE9B2Sb4Ws/mR/+0AgA/JUtsv1ytlRSH3/pDIA==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
"integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
"dependencies": {
"@vue/compiler-dom": "3.4.4",
"@vue/compiler-sfc": "3.4.4",
"@vue/runtime-dom": "3.4.4",
"@vue/server-renderer": "3.4.4",
"@vue/shared": "3.4.4"
"@vue/compiler-dom": "3.4.15",
"@vue/compiler-sfc": "3.4.15",
"@vue/runtime-dom": "3.4.15",
"@vue/server-renderer": "3.4.15",
"@vue/shared": "3.4.15"
},
"peerDependencies": {
"typescript": "*"
@ -17675,9 +17704,9 @@
}
},
"node_modules/xslt3/node_modules/axios": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.4.tgz",
"integrity": "sha512-heJnIs6N4aa1eSthhN9M5ioILu8Wi8vmQW9iHQ9NUvfkJb0lEEDUiIdQNAuBtfUt3FxReaKdpQA5DbmMOqzF/A==",
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.4",

View File

@ -91,6 +91,7 @@
"leaflet": "^1.9.3",
"luxon": "^3.2.1",
"node-2fa": "^2.0.3",
"node-exceptions": "^4.0.1",
"notiwind": "^2.0.0",
"pg": "^8.9.0",
"proxy-addr": "^2.0.7",
@ -99,6 +100,7 @@
"reflect-metadata": "^0.2.1",
"saxon-js": "^2.5.0",
"source-map-support": "^0.5.21",
"toastify-js": "^1.12.0",
"vuedraggable": "^4.1.0",
"xmlbuilder2": "^3.1.1"
}

View File

@ -21,5 +21,6 @@
"assets/fonts/inter-vietnamese-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-vietnamese-400-normal.5952d3d3.woff2",
"assets/images/marker-icon.png": "http://localhost:8080/assets/images/marker-icon.2b3e1faf.png",
"assets/images/layers-2x.png": "http://localhost:8080/assets/images/layers-2x.8f2c4d11.png",
"assets/images/layers.png": "http://localhost:8080/assets/images/layers.416d9136.png"
"assets/images/layers.png": "http://localhost:8080/assets/images/layers.416d9136.png",
"assets/images/Close.svg": "http://localhost:8080/assets/images/Close.e4887675.svg"
}

View File

@ -0,0 +1,246 @@
<template>
<!-- <div id="twofactor-totp-settings">
<template v-if="loading">
<span class="icon-loading-small totp-loading" />
<span> {{ t('twofactor_totp', 'Enable TOTP') }} </span>
</template>
<div v-else>
<input id="totp-enabled" v-model="enabled" type="checkbox" class="checkbox" :disabled="loading"
@change="toggleEnabled">
<label for="totp-enabled">{{
t('twofactor_totp', 'Enable TOTP')
}}</label>
</div>
<SetupConfirmation v-if="secret" :secret="secret" :qr-url="qrUrl" :loading="loadingConfirmation"
:confirmation.sync="confirmation" @confirm="enableTOTP" />
</div> -->
<CardBox :icon="mdiTwoFactorAuthentication" id="twofactor-totp-settings" title="Two-Factor Authentication" form>
<template v-if="loading">
<!-- <span class="icon-loading-small totp-loading" /> -->
<div class="relative inline-flex">
<div class="w-6 h-6 bg-blue-500 rounded-full"></div>
<div class="w-6 h-6 bg-blue-500 rounded-full absolute top-0 left-0 animate-ping"></div>
<div class="w-6 h-6 bg-blue-500 rounded-full absolute top-0 left-0 animate-pulse"></div>
<span class="ml-4 max-w-xl text-sm text-gray-600">Enabling TOTP...</span>
</div>
</template>
<div v-else>
<!-- <div class="text-lg font-medium text-gray-900">
You have not enabled two factor authentication.
</div>
<div class="text-sm text-gray-600">
When two factor authentication is enabled, you will be prompted for a secure,
random token during authentication. You may retrieve this token from your phone's
Google Authenticator application.
</div> -->
<input id="totp-enabled" v-model="enabled" type="checkbox" class="checkbox" :disabled="loading"
@change="toggleEnabled" />
<!-- <label for="totp-enabled"> Enable TOTP </label> -->
<label for="totp-enabled">{{ checkboxLabel }}</label>
</div>
<!-- <SetupConfirmation v-if="secret" :secret="secret" :qr-url="qrUrl" :loading="loadingConfirmation"
:confirmation.sync="confirmation" @confirm="enableTOTP" /> -->
<div v-if="qrSecret != ''">
<div class="mt-4 max-w-xl text-sm text-gray-600">
<!-- <p class="font-semibold">
Two factor authentication is now enabled.
</p> -->
<p>Your new TOTP secret is: {{ qrSecret }}</p>
<p>For quick setup, scan this QR code with your phone's authenticator application (TOTP):</p>
<div class="mt-4">
<img :src="qrSvg" />
</div>
</div>
<!-- <div class="mt-4 max-w-xl text-sm text-gray-600">
<p>
After you configured your app, enter a test code below to ensure everything works correctly:
</p>
</div> -->
<div class="mt-4 max-w-xl text-sm text-gray-600">
<p>After you configured your app, enter a test code below to ensure everything works correctly:</p>
<!-- :disabled="loading" -->
<input id="totp-confirmation" :disabled="loadingConfirmation" v-model="confirmationCode" type="tel"
minlength="6" maxlength="10" autocomplete="off" autocapitalize="off"
:placeholder="'Authentication code'" @keydown="onConfirmKeyDown" />
<!-- <input id="totp-confirmation-submit" type="button" :disabled="loading" :value="'Verify'"
@click="enableTOTP"> -->
<!-- <BaseButtons>
<BaseButton :icon="mdiContentSaveCheck" type="button" :disabled="loadingConfirmation" color="info"
label="Verify" @click="enableTOTP" />
</BaseButtons> -->
</div>
</div>
<template #footer>
<BaseButtons v-if="qrSecret != ''">
<BaseButton :icon="mdiContentSaveCheck" type="button" :disabled="loadingConfirmation" color="info"
label="Verify" @click="enableTOTP" />
</BaseButtons>
</template>
</CardBox>
</template>
<script setup lang="ts">
import CardBox from '@/Components/CardBox.vue';
import { computed, ref } from 'vue';
import { MainService, State } from '@/Stores/main';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import Notification from '@/utils/toast';
import { mdiContentSaveCheck, mdiTwoFactorAuthentication } from '@mdi/js';
const mainService = MainService();
const emit = defineEmits(['confirm', 'update:confirmation']);
const props = defineProps({
// user will be returned from controller action
// user: {
// type: Object,
// default: () => ({}),
// },
twoFactorEnabled: {
type: Boolean,
default: false,
},
// // code: {
// // type: Object,
// // },
// // recoveryCodes: {
// // type: Array<string>,
// // default: () => [],
// // },
// // errors: {
// // type: Object,
// // default: () => ({}),
// // },
});
let loading = ref(false);
let loadingConfirmation = ref(false);
let test;
if (props.twoFactorEnabled) {
test = State.STATE_ENABLED;
} else {
test = State.STATE_DISABLED;
}
mainService.setState(test);
const enabled = ref(mainService.totpState == State.STATE_ENABLED);
let qrSecret = ref('');
let qrUrl = ref('');
let qrSvg = ref('');
const confirmationCode = ref('');
const confirm = () => {
emit('update:confirmation', confirmationCode.value);
emit('confirm');
};
const onConfirmKeyDown = (e) => {
if (e.which === 13) {
confirm();
}
};
const state = computed(() => mainService.totpState);
const checkboxLabel = computed(() => {
if (enabled.value == true) {
return ' Disable TOTP';
} else {
return ' Enable TOTP';
}
});
const toggleEnabled = async () => {
if (loading.value == true) {
// Ignore event
// Logger.debug('still loading -> ignoring event')
return;
}
if (enabled.value) {
return await createTOTP();
} else {
return await disableTOTP();
}
};
const createTOTP = async () => {
// Show loading spinner
loading.value = true;
// Logger.debug('starting setup')
try {
const { url, secret, svg } = await mainService.create();
qrSecret.value = secret;
qrUrl.value = url;
qrSvg.value = svg;
// If the stat could be changed, keep showing the loading
// spinner until the user has finished the registration
// if state isCretaed, show loading:
loading.value = state.value === State.STATE_CREATED;
} catch (e) {
Notification.showWarning('Could not enable TOTP');
// Logger.error('Could not enable TOTP', e)
console.log('Could not create TOTP', e.message);
// Restore on error
loading.value = false;
enabled.value = false;
}
};
const disableTOTP = async () => {
loading.value = false;
// Logger.debug('starting disable');
await mainService.disable();
enabled.value = false;
loading.value = false;
Notification.showSuccess('TOTP disabled!');
};
const enableTOTP = async () => {
loading.value = true;
loadingConfirmation.value = true;
try {
await mainService.confirm(confirmationCode.value);
if (mainService.totpState === State.STATE_ENABLED) {
// Success
loading.value = false;
enabled.value = true;
qrUrl.value = '';
qrSecret.value = '';
Notification.showSuccess('two factor authentication enabled');
} else {
Notification.showWarning('Could not verify your key. Please try again');
console.log('Could not verify your key. Please try again');
}
confirmationCode.value = '';
loadingConfirmation.value = false;
} catch (e) {
console.log('Could not enable TOTP', e.message);
Notification.showWarning('Could not enable TOTP ' + e.message);
confirmationCode.value = '';
loadingConfirmation.value = false;
}
};
</script>
<style scoped>
.totp-loading {
display: inline-block;
vertical-align: sub;
margin-left: -2px;
margin-right: 4px;
}
</style>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
// import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
import { useForm, router } from '@inertiajs/vue3';
import { useForm } from '@inertiajs/vue3';
// import { ref } from 'vue';
// import { reactive } from 'vue';
import {
mdiAccount,
@ -11,7 +12,6 @@ import {
mdiFormTextboxPassword,
mdiArrowLeftBoldOutline,
mdiAlertBoxOutline,
mdiInformation
} from '@mdi/js';
import SectionMain from '@/Components/SectionMain.vue';
import CardBox from '@/Components/CardBox.vue';
@ -28,10 +28,13 @@ import { stardust } from '@eidellev/adonis-stardust/client';
import { computed, Ref } from 'vue';
import { usePage } from '@inertiajs/vue3';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
// import { Inertia } from '@inertiajs/inertia';
import PersonalTotpSettings from '@/Components/PersonalTotpSettings.vue';
// import { MainService } from '@/Stores/main';
// const mainService = MainService();
const emit = defineEmits(['confirm', 'update:confirmation'])
const props = defineProps({
defineProps({
// user will be returned from controller action
user: {
type: Object,
@ -58,12 +61,12 @@ const props = defineProps({
// login: props.user.login,
// email: props.user.email,
// });
const enableTwoFactorAuthentication = async () => {
await router.post(stardust.route('account.password.enable2fa'));
};
const disableTwoFactorAuthentication = async () => {
await router.post(stardust.route('account.password.disable2fa'));
};
// const enableTwoFactorAuthentication = async () => {
// await router.post(stardust.route('account.password.enable2fa'));
// };
// const disableTwoFactorAuthentication = async () => {
// await router.post(stardust.route('account.password.disable2fa'));
// };
const passwordForm = useForm({
@ -84,6 +87,28 @@ const passwordSubmit = async () => {
const flash: Ref<any> = computed(() => {
return usePage().props.flash;
});
// const confirmationCode = ref('');
// const confirm = () => {
// emit('update:confirmation', confirmationCode);
// emit('confirm');
// };
// const onConfirmKeyDown = (e) => {
// if (e.which === 13) {
// confirm()
// }
// };
// const generateSecretCode = (user) => {
// const secret = generateSecret({
// name: 'TethysCloud',
// account: user.email,
// });
// return secret.secret;
// }
</script>
<template>
@ -101,8 +126,8 @@ const flash: Ref<any> = computed(() => {
{{ $page.props.flash.message }}
</NotificationBar> -->
<!-- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> -->
<div class="grid grid-cols-1 lg:grid-cols-1 gap-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- <div class="grid grid-cols-1 lg:grid-cols-1 gap-6"> -->
<!-- password form -->
<!-- <CardBox title="Edit Profile" :icon="mdiAccountCircle" form @submit.prevent="profileForm.post(route('admin.account.info.store'))"> -->
@ -186,24 +211,9 @@ const flash: Ref<any> = computed(() => {
<!-- <CardBox title="Edit Profile" :icon="mdiAccountCircle" form @submit.prevent="profileForm.post(route('admin.account.info.store'))"> -->
<CardBox v-if="!props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled"/>
<!-- <CardBox v-if="!props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form
@submit.prevent="enableTwoFactorAuthentication()">
<!-- <FormField label="Login" help="Required. Your login name" :class="{ 'text-red-400': errors.login }">
<FormControl v-model="factorForm.login" v-bind:icon="mdiAccount" name="login" required :error="errors.login">
<div class="text-red-400 text-sm" v-if="errors.login">
{{ errors.login }}
</div>
</FormControl>
</FormField>
<FormField label="Email" help="Required. Your e-mail" :class="{ 'text-red-400': errors.email }">
<FormControl v-model="factorForm.email" :icon="mdiMail" type="email" name="email" required :error="errors.email">
<div class="text-red-400 text-sm" v-if="errors.email">
{{ errors.email }}
</div>
</FormControl>
</FormField> -->
<div class="text-lg font-medium text-gray-900">
You have not enabled two factor authentication.
</div>
@ -218,70 +228,9 @@ const flash: Ref<any> = computed(() => {
<BaseButton color="info" type="submit" label="Enable" />
</BaseButtons>
</template>
</CardBox>
</CardBox> -->
<CardBox v-else-if="props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form @submit.prevent="disableTwoFactorAuthentication()">
<!-- <div class="w-1/2 space-y-4 bg-gray-100 p-8"> -->
<h3 class="text-lg font-medium text-gray-900">
You have enabled two factor authentication.
</h3>
<div class="mt-3 max-w-xl text-sm text-gray-600">
<p>
When two factor authentication is enabled, you will be prompted for a secure, random
token during authentication. You may retrieve this token from your phone's Google
Authenticator application.
</p>
</div>
<div v-if="code">
<div class="mt-4 max-w-xl text-sm text-gray-600">
<p class="font-semibold">
Two factor authentication is now enabled. Scan the following QR code using your
phone's authenticator application.
</p>
</div>
<div class="mt-4">
<img :src="code?.svg" />
</div>
</div>
<!-- @if(recoveryCodes) -->
<div v-if="recoveryCodes" class="mt-4 max-w-xl text-sm text-gray-600">
<p class="font-semibold">
Store these recovery codes in a secure password manager. They can be used to recover
access to your account if your two factor authentication device is lost.
</p>
</div>
<!-- <div class="mt-4 grid max-w-xl gap-1 rounded-lg bg-gray-100 px-4 py-4 font-mono text-sm">
@each(code in recoveryCodes)
<div>
{{ code }}
</div>
@endeach
</div> -->
<!-- @endif -->
<div class="flex justify-between">
<!-- <form action="{{ route('UserController.fetchRecoveryCodes') }}" method="GET">
<button type="submit" class="px-auto items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs
font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none
">
Show Recovery Codes
</button>
</form>
<form action="{{ route('UserController.disableTwoFactorAuthentication') }}" method="POST">
<button type="submit" class="px-auto items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs
font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none
">
Disable
</button>
</form> -->
<BaseButton color="info" type="submit" label="Disable" />
</div>
<!-- </div> -->
</CardBox>
</div>
</SectionMain>

View File

@ -22,6 +22,26 @@ interface TransactionItem {
business: string;
}
export enum State {
STATE_DISABLED = 0,
STATE_CREATED = 1,
STATE_ENABLED = 2,
}
export const saveState = async (data) => {
const url = '/api/twofactor_totp/settings/enable';
const resp = await axios.post(url, data);
return resp.data;
};
// Anfrage staet : 1
// ANtwort json:
// state: 1
// secret:"OX7IQ4OI3GXGFPHY"
// qUrl:"https://odysseus.geologie.ac.at/apps/twofactor_totp/settings/enable"
export const MainService = defineStore('main', {
state: () => ({
/* User */
@ -45,6 +65,8 @@ export const MainService = defineStore('main', {
dataset: {} as Dataset,
menu: menu,
totpState: 0,
}),
actions: {
// payload = authenticated user
@ -113,6 +135,36 @@ export const MainService = defineStore('main', {
});
},
setState(state) {
this.totpState = state;
},
async create(): Promise<{ url: any; secret: any; svg: any }> {
const { state, secret, url, svg } = await saveState({ state: State.STATE_CREATED });
this.totpState = state;
return { url, secret, svg };
// .then(({ state, secret, qrUrl }) => {
// this.totpState = state;
// return { qrUrl, secret };
// })
// .catch((error) => {
// alert(error.message);
// });
},
async disable() {
const { state } = await saveState({ state: State.STATE_DISABLED });
this.totpState = state;
},
async confirm(code: string) {
const { state } = await saveState({
state: State.STATE_ENABLED,
code,
});
this.totpState = state;
},
// fetchfiles(id) {
// // sampleDataKey= authors or datasets
// axios

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">
<path d="M14 12.3L12.3 14 8 9.7 3.7 14 2 12.3 6.3 8 2 3.7 3.7 2 8 6.3 12.3 2 14 3.7 9.7 8z"/>
</svg>

After

Width:  |  Height:  |  Size: 170 B

View File

@ -0,0 +1,103 @@
/* remember to import this scss file into your app */
.toastify.dialogs {
min-width: 200px;
background: none;
background-color: var(--color-main-background);
color: var(--color-main-text);
box-shadow: 0 0 6px 0 var(--color-box-shadow);
padding: 0 12px;
margin-top: 45px;
position: fixed;
z-index: 10100;
border-radius: var(--border-radius);
display: flex;
align-items: center;
.toast-undo-container {
display: flex;
align-items: center;
}
.toast-undo-button,
.toast-close {
position: static;
overflow: hidden;
box-sizing: border-box;
min-width: 44px;
height: 100%;
padding: 12px;
white-space: nowrap;
background-repeat: no-repeat;
background-position: center;
background-color: transparent;
min-height: 0;
/* icon styling */
&.toast-close {
text-indent: 0;
opacity: 0.4;
border: none;
min-height: 44px;
margin-left: 10px;
font-size: 0;
/* dark theme overrides for Nextcloud 25 and later */
&::before {
background-image: url('./Close.svg');
content: ' ';
filter: var(--background-invert-if-dark);
display: inline-block;
width: 16px;
height: 16px;
}
}
&.toast-undo-button {
/* $margin: 3px; */
/* margin: $margin; */
/* height: calc(100% - 2 * #{$margin}); */
margin-left: 12px;
}
&:hover,
&:focus,
&:active {
cursor: pointer;
opacity: 1;
}
}
&.toastify-top {
right: 10px;
}
/* Toast with onClick callback */
&.toast-with-click {
cursor: pointer;
}
/* Various toasts types */
&.toast-error {
border-left: 3px solid var(--color-error);
}
&.toast-info {
border-left: 3px solid var(--color-primary);
}
&.toast-warning {
border-left: 3px solid var(--color-warning);
}
&.toast-success {
border-left: 3px solid var(--color-success);
}
&.toast-undo {
border-left: 3px solid var(--color-success);
}
}

216
resources/js/utils/toast.ts Normal file
View File

@ -0,0 +1,216 @@
import Toastify from 'toastify-js';
// import { t } from './utils/l10n.js';
import './toast.css';
/**
* Enum of available Toast types
*/
export enum ToastType {
ERROR = 'toast-error',
WARNING = 'toast-warning',
INFO = 'toast-info',
SUCCESS = 'toast-success',
PERMANENT = 'toast-error',
UNDO = 'toast-undo',
}
/** @deprecated Use ToastAriaLive.OFF */
export const TOAST_ARIA_LIVE_OFF = 'off';
/** @deprecated Use ToastAriaLive.POLITE */
export const TOAST_ARIA_LIVE_POLITE = 'polite';
/** @deprecated Use ToastAriaLive.ASSERTIVE */
export const TOAST_ARIA_LIVE_ASSERTIVE = 'assertive';
export enum ToastAriaLive {
OFF = TOAST_ARIA_LIVE_OFF,
POLITE = TOAST_ARIA_LIVE_POLITE,
ASSERTIVE = TOAST_ARIA_LIVE_ASSERTIVE,
}
/** Timeout in ms of a undo toast */
export const TOAST_UNDO_TIMEOUT = 10000;
/** Default timeout in ms of toasts */
export const TOAST_DEFAULT_TIMEOUT = 4000;
/** Timeout value to show a toast permanently */
export const TOAST_PERMANENT_TIMEOUT = -1;
/**
* Type of a toast
* @see https://apvarun.github.io/toastify-js/
* @notExported
*/
type Toast = ReturnType<typeof Toastify>;
export interface ToastOptions {
/**
* Defines the timeout in milliseconds after which the toast is closed. Set to -1 to have a persistent toast.
*/
timeout?: number;
/**
* Set to true to allow HTML content inside of the toast text
* @default false
*/
isHTML?: boolean;
/**
* Set a type of {ToastType} to style the modal
*/
type?: ToastType;
/**
* Provide a function that is called after the toast is removed
*/
onRemove?: () => void;
/**
* Provide a function that is called when the toast is clicked
*/
onClick?: () => void;
/**
* Make the toast closable
*/
close?: boolean;
/**
* Specify the element to attach the toast element to (for testing)
*/
selector?: string;
/**
* Whether the messages should be announced to screen readers.
* See the following docs for an explanation when to use which:
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
*
* By default, errors are announced assertive and other messages "polite".
*/
ariaLive?: ToastAriaLive;
}
/**
* Show a toast message
*
* @param data Message to be shown in the toast, any HTML is removed by default
* @param options
*/
export function showMessage(data: string | Node, options?: ToastOptions): Toast {
options = Object.assign(
{
timeout: TOAST_DEFAULT_TIMEOUT,
isHTML: false,
type: undefined,
// An undefined selector defaults to the body element
selector: undefined,
onRemove: () => {},
onClick: undefined,
close: true,
},
options,
);
if (typeof data === 'string' && !options.isHTML) {
// fime mae sure that text is extracted
const element = document.createElement('div');
element.innerHTML = data;
data = element.innerText;
}
let classes = options.type ?? '';
if (typeof options.onClick === 'function') {
classes += ' toast-with-click ';
}
const isNode = data instanceof Node;
let ariaLive: ToastAriaLive = ToastAriaLive.POLITE;
if (options.ariaLive) {
ariaLive = options.ariaLive;
} else if (options.type === ToastType.ERROR || options.type === ToastType.UNDO) {
ariaLive = ToastAriaLive.ASSERTIVE;
}
const toast = Toastify({
[!isNode ? 'text' : 'node']: data,
duration: options.timeout,
callback: options.onRemove,
onClick: options.onClick,
close: options.close,
gravity: 'top',
selector: options.selector,
position: 'right',
backgroundColor: '',
className: 'dialogs ' + classes,
escapeMarkup: !options.isHTML,
ariaLive,
});
toast.showToast();
return toast;
}
export default {
updatableNotification: null,
getDefaultNotificationFunction: null,
/**
* Shows a notification that disappears after x seconds, default is
* 7 seconds
*
* @param {string} text Message to show
* @param {Array} [options] options array
* @param {number} [options.timeout=7] timeout in seconds, if this is 0 it will show the message permanently
* @param {boolean} [options.isHTML=false] an indicator for HTML notifications (true) or text (false)
* @param {string} [options.type] notification type
* @return {JQuery} the toast element
*/
showTemporary(text, options = { timeout: 3000 }) {
options = options || {};
options.timeout = options.timeout || TOAST_DEFAULT_TIMEOUT;
const toast = showMessage(text, options);
toast.toastElement.toastify = toast;
// return $(toast.toastElement)
},
/**
* Show a toast message with error styling
*
* @param text Message to be shown in the toast, any HTML is removed by default
* @param options
*/
showError(text: string, options?: ToastOptions): Toast {
return showMessage(text, { ...options, type: ToastType.ERROR });
},
/**
* Show a toast message with warning styling
*
* @param text Message to be shown in the toast, any HTML is removed by default
* @param options
*/
showWarning(text: string, options?: ToastOptions): Toast {
return showMessage(text, { ...options, type: ToastType.WARNING });
},
/**
* Show a toast message with info styling
*
* @param text Message to be shown in the toast, any HTML is removed by default
* @param options
*/
showInfo(text: string, options?: ToastOptions): Toast {
return showMessage(text, { ...options, type: ToastType.INFO });
},
/**
* Show a toast message with success styling
*
* @param text Message to be shown in the toast, any HTML is removed by default
* @param options
*/
showSuccess(text: string, options?: ToastOptions): Toast {
return showMessage(text, { ...options, type: ToastType.SUCCESS });
},
};

View File

@ -19,7 +19,10 @@ Route.group(() => {
Route.get('/download/:id', 'FileController.findOne').as('file.findOne');
Route.get('/avatar/:name/:background?/:textColor?/:size?', 'AvatarController.generateAvatar')
Route.get('/avatar/:name/:background?/:textColor?/:size?', 'AvatarController.generateAvatar');
Route.post('/twofactor_totp/settings/enable/:state/:code?', 'UserController.enable').as('apps.twofactor_totp.enable') .middleware(['auth']);;
});
// .middleware("auth:api");
})