diff --git a/frontend/app/src/app/modules/mail/mail-login.component.ts b/frontend/app/src/app/modules/mail/mail-login.component.ts index 44c9fe55..044de217 100644 --- a/frontend/app/src/app/modules/mail/mail-login.component.ts +++ b/frontend/app/src/app/modules/mail/mail-login.component.ts @@ -1,4 +1,13 @@ -import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {AsyncPipe, TitleCasePipe} from '@angular/common'; +import {ChangeDetectionStrategy, Component, WritableSignal, computed, inject, signal} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {IonicModule} from '@ionic/angular'; +import {TranslateModule} from '@ngx-translate/core'; +import {IonIconModule} from 'src/app/util/ion-icon/ion-icon.module'; +import {UtilModule} from 'src/app/util/util.module'; +import {MailService} from './mail.service'; +import {Observable, of, map, catchError, startWith, shareReplay, tap, take} from 'rxjs'; +import {ActivatedRoute, Router} from '@angular/router'; @Component({ selector: 'stapps-mail-login', @@ -6,5 +15,50 @@ import {ChangeDetectionStrategy, Component} from '@angular/core'; styleUrl: 'mail-login.scss', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, + imports: [IonicModule, UtilModule, TranslateModule, TitleCasePipe, IonIconModule, FormsModule, AsyncPipe], }) -export class MailLoginComponent {} +export class MailLoginComponent { + showPassword = signal(false); + + email = signal(''); + + password = signal(''); + + error: WritableSignal> = signal(of(undefined)); + + loading = computed(() => + this.error().pipe( + map(() => false), + startWith(true), + ), + ); + + mailService = inject(MailService); + + router = inject(Router); + + activatedRoute = inject(ActivatedRoute); + + submit(event: SubmitEvent) { + event.preventDefault(); + const form = event.target as HTMLFormElement; + if (form.checkValidity()) { + this.error.set( + this.mailService.login(this.email(), this.password()).pipe( + tap(success => { + if (success) { + this.activatedRoute.data.pipe(take(1)).subscribe(data => { + this.router.navigate(data.redirectTo); + }); + } + }), + map(success => (success ? undefined : 'mail.login.error.INVALID_CREDENTIALS')), + catchError(error => of(error.message)), + shareReplay(1), + ), + ); + } else { + form.reportValidity(); + } + } +} diff --git a/frontend/app/src/app/modules/mail/mail-login.html b/frontend/app/src/app/modules/mail/mail-login.html index e69de29b..9541dffa 100644 --- a/frontend/app/src/app/modules/mail/mail-login.html +++ b/frontend/app/src/app/modules/mail/mail-login.html @@ -0,0 +1,67 @@ + + + + + + + + + +

{{ 'mail.login.TITLE' | translate | titlecase }}

+
+ + + + + + + + + + @if (error() | async; as error) { + {{ error | translate | titlecase }} + } + + + {{ 'mail.login.LOGIN' | translate | titlecase }} + +
+
diff --git a/frontend/app/src/app/modules/mail/mail-login.scss b/frontend/app/src/app/modules/mail/mail-login.scss index e69de29b..16b401e9 100644 --- a/frontend/app/src/app/modules/mail/mail-login.scss +++ b/frontend/app/src/app/modules/mail/mail-login.scss @@ -0,0 +1,17 @@ +h1 { + margin-inline: auto; + color: var(--ion-color-primary-contrast); +} + +form { + display: flex; + flex-direction: column; + align-items: flex-end; + + max-width: 30em; + margin: var(--spacing-xxl) auto; + padding: var(--spacing-xl); + + background: var(--ion-item-background); + border-radius: var(--border-radius-default); +} diff --git a/frontend/app/src/app/modules/mail/mail-page.component.ts b/frontend/app/src/app/modules/mail/mail-page.component.ts index 743b2c3e..688f296c 100644 --- a/frontend/app/src/app/modules/mail/mail-page.component.ts +++ b/frontend/app/src/app/modules/mail/mail-page.component.ts @@ -8,6 +8,9 @@ import {UtilModule} from 'src/app/util/util.module'; import {FormatPurePipeModule, IsTodayPipeModule} from 'ngx-date-fns'; import {ActivatedRoute, RouterModule} from '@angular/router'; import {combineLatest, map, mergeMap} from 'rxjs'; +import {MailStorageProvider} from './mail-storage.provider'; +import {MailboxTreeItem} from './schema'; +import {SCIcon} from 'src/app/util/ion-icon/icon'; @Component({ selector: 'stapps-mail-page', @@ -24,6 +27,7 @@ import {combineLatest, map, mergeMap} from 'rxjs'; FormatPurePipeModule, IsTodayPipeModule, RouterModule, + IonIconModule, ], }) export class MailPageComponent { @@ -31,6 +35,19 @@ export class MailPageComponent { readonly mailService = inject(MailService); + readonly mailStorage = inject(MailStorageProvider); + + mailIcons: Record, keyof typeof SCIcon> = { + '\\Inbox': SCIcon.inbox, + '\\All': SCIcon.all_inbox, + '\\Archive': SCIcon.archive, + '\\Drafts': SCIcon.drafts, + '\\Flagged': SCIcon.flag, + '\\Junk': SCIcon.folder, + '\\Sent': SCIcon.send, + '\\Trash': SCIcon.delete, + }; + mailbox = this.activatedRoute.paramMap.pipe(map(parameters => parameters.get('mailbox')!)); mails = this.mailbox.pipe( diff --git a/frontend/app/src/app/modules/mail/mail-page.html b/frontend/app/src/app/modules/mail/mail-page.html index 5339393e..9419187a 100644 --- a/frontend/app/src/app/modules/mail/mail-page.html +++ b/frontend/app/src/app/modules/mail/mail-page.html @@ -1,5 +1,10 @@ + + + + + Mail @@ -7,37 +12,63 @@

Inbox

- @if (mails | async; as mails) { - - @for (mail of mails; track mail) { - -
- @if (mail.from; as from) { -
- {{ (from.value.name || from.value.address)?.charAt(0)?.toUpperCase() }} -
- } -
- -

{{ mail.from.value.name || mail.from.value.address }}

- @if (mail.subject) { -

{{ mail.subject.value }}

- } -
- - @if (mail.date.value | dfnsIsToday) { - {{ mail.date.value | dfnsFormatPure: 'p' }} - } @else { - {{ mail.date.value | dfnsFormatPure: 'P' }} - } - -
+ + + @if (mailStorage.mailboxes | async; as mailboxes) { + + @for (folder of mailboxes.folders; track folder) { + + @if (folder.specialUse) { + + } @else { + + } + {{ folder.name }} + + } + } -
- } @else { -
Loading...
- } + + + @if (mails | async; as mails) { + + @for (mail of mails; track mail) { + +
+ @if (mail.from; as from) { +
+ {{ (from.value.name || from.value.address)?.charAt(0)?.toUpperCase() }} +
+ } +
+ +

{{ mail.from.value.name || mail.from.value.address }}

+ @if (mail.subject) { +

{{ mail.subject.value }}

+ } +
+ + @if (mail.date.value | dfnsIsToday) { + {{ mail.date.value | dfnsFormatPure: 'p' }} + } @else { + {{ mail.date.value | dfnsFormatPure: 'P' }} + } + +
+ } +
+ } @else { +
Loading...
+ } +
diff --git a/frontend/app/src/app/modules/mail/mail-page.scss b/frontend/app/src/app/modules/mail/mail-page.scss index c484561d..c8c05341 100644 --- a/frontend/app/src/app/modules/mail/mail-page.scss +++ b/frontend/app/src/app/modules/mail/mail-page.scss @@ -27,6 +27,12 @@ ion-item p { white-space: nowrap; } +ion-menu, +ion-menu::part(container), +ion-menu::part(backdrop) { + background: none; +} + ion-list { margin: var(--spacing-md); border-radius: var(--border-radius-default); diff --git a/frontend/app/src/app/modules/mail/mail-storage.provider.ts b/frontend/app/src/app/modules/mail/mail-storage.provider.ts index 85ff6433..8628665a 100644 --- a/frontend/app/src/app/modules/mail/mail-storage.provider.ts +++ b/frontend/app/src/app/modules/mail/mail-storage.provider.ts @@ -1,29 +1,140 @@ +/* eslint-disable unicorn/no-useless-undefined */ import {Injectable} from '@angular/core'; -import {StorageProvider} from '../storage/storage.provider'; -import {Email} from 'postal-mime'; +import {Capacitor} from '@capacitor/core'; +import {SecureStoragePlugin} from 'capacitor-secure-storage-plugin'; +import { + BehaviorSubject, + Observable, + catchError, + defer, + distinctUntilChanged, + from, + fromEvent, + map, + merge, + mergeMap, + of, + shareReplay, + take, +} from 'rxjs'; +import {MailboxTreeRoot} from './schema'; +import equal from 'fast-deep-equal'; @Injectable({providedIn: 'root'}) export class MailStorageProvider { - constructor(readonly storageProvider: StorageProvider) {} + static readonly DB_NAME = 'mail'; - private storageKey(...path: string[]): string { - return ['mail', ...path.map(encodeURIComponent)].join('/'); + static readonly MAILBOX_STORE_NAME = 'mailboxes'; + + static readonly EMAIL_STORE_NAME = 'emails'; + + static readonly CREDENTIALS_KEY = 'email-credentials'; + + database = defer(() => { + const request = indexedDB.open(MailStorageProvider.DB_NAME, 1); + return merge( + fromEvent(request, 'upgradeneeded').pipe( + map(event => { + const database = (event.target as IDBOpenDBRequest).result; + database.createObjectStore(MailStorageProvider.MAILBOX_STORE_NAME, {keyPath: 'path'}); + database.createObjectStore(MailStorageProvider.EMAIL_STORE_NAME, {keyPath: 'id'}); + return database; + }), + ), + fromEvent(request, 'success').pipe( + take(1), + map(event => (event.target as IDBOpenDBRequest).result), + ), + fromEvent(request, 'error').pipe( + take(1), + map(event => { + throw (event.target as IDBOpenDBRequest).error; + }), + ), + fromEvent(request, 'blocked').pipe( + take(1), + map(() => { + throw new Error('Database blocked'); + }), + ), + ); + }).pipe(shareReplay(1)); + + private mailboxesChanged = new BehaviorSubject(undefined); + + mailboxes: Observable = this.database.pipe( + mergeMap(database => + merge(this.mailboxesChanged, of(undefined)).pipe( + mergeMap(() => { + const request: IDBRequest = database + .transaction([MailStorageProvider.MAILBOX_STORE_NAME], 'readonly') + .objectStore(MailStorageProvider.MAILBOX_STORE_NAME) + .get(''); + return merge( + fromEvent(request, 'success').pipe(map(() => request.result as MailboxTreeRoot)), + fromEvent(request, 'error').pipe( + map(event => { + throw (event.target as IDBRequest).error; + }), + ), + ).pipe(take(1)); + }), + ), + ), + distinctUntilChanged((a, b) => equal(a, b)), + ); + + setMailboxes(root: MailboxTreeRoot | undefined): Observable { + return this.database.pipe( + mergeMap(database => { + const transaction = database.transaction([MailStorageProvider.MAILBOX_STORE_NAME], 'readwrite'); + const store = transaction.objectStore(MailStorageProvider.MAILBOX_STORE_NAME); + store.clear(); + console.log(root); + if (root !== undefined) { + store.add(root); + } + return merge( + fromEvent(transaction, 'complete').pipe( + map(() => { + this.mailboxesChanged.next(); + }), + ), + fromEvent(transaction, 'error').pipe( + map(event => { + throw (event.target as IDBRequest).error; + }), + ), + ).pipe(take(1)); + }), + ); } - async get(mailbox: string, id: string): Promise { - try { - return await this.storageProvider.get(this.storageKey(mailbox, id)); - } catch (error) { - console.info('Mail not found in storage', error); - return undefined; + private credentialsChanged = new BehaviorSubject(undefined); + + credentials: Observable = this.credentialsChanged.pipe( + mergeMap(() => { + return Capacitor.isNativePlatform() + ? from(SecureStoragePlugin.get({key: MailStorageProvider.CREDENTIALS_KEY})).pipe( + map(({value}) => value), + catchError(() => of(undefined)), + ) + : of(localStorage.getItem(MailStorageProvider.CREDENTIALS_KEY) ?? undefined); + }), + ); + + async setCredentials(credentials: string | undefined): Promise { + if (Capacitor.isNativePlatform()) { + await (credentials === undefined + ? SecureStoragePlugin.remove({key: MailStorageProvider.CREDENTIALS_KEY}) + : SecureStoragePlugin.set({key: MailStorageProvider.CREDENTIALS_KEY, value: credentials})); + } else { + if (credentials === undefined) { + localStorage.removeItem(MailStorageProvider.CREDENTIALS_KEY); + } else { + localStorage.setItem(MailStorageProvider.CREDENTIALS_KEY, credentials); + } } - } - - async put(mailbox: string, id: string, email: Email): Promise { - await this.storageProvider.put(this.storageKey(mailbox, id), email); - } - - async delete(mailbox: string, id: string): Promise { - await this.storageProvider.delete(this.storageKey(mailbox, id)); + this.credentialsChanged.next(); } } diff --git a/frontend/app/src/app/modules/mail/mail.module.ts b/frontend/app/src/app/modules/mail/mail.module.ts index 174136b1..6df5d870 100644 --- a/frontend/app/src/app/modules/mail/mail.module.ts +++ b/frontend/app/src/app/modules/mail/mail.module.ts @@ -12,26 +12,37 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {RouterModule, UrlSegment, UrlSegmentGroup, UrlTree} from '@angular/router'; +import {Router, RouterModule} from '@angular/router'; import {NgModule, inject} from '@angular/core'; import {MailService} from './mail.service'; +import {map, take} from 'rxjs'; + +function mailLoginGuard() { + const router = inject(Router); + return inject(MailService).isLoggedIn.pipe( + map(isLoggedIn => (isLoggedIn ? true : router.createUrlTree(['/mail-login']))), + take(1), + ); +} @NgModule({ imports: [ RouterModule.forChild([ { - path: 'mail', + path: 'mail-login', + data: {redirectTo: ['/mail']}, loadComponent: () => import('./mail-login.component').then(m => m.MailLoginComponent), - canActivateChild: [ - () => { - if (inject(MailService).isLoggedIn()) { - return true; - } else { - return new UrlTree(new UrlSegmentGroup([new UrlSegment('/mail', {})], {})); - } - }, - ], + }, + { + path: 'mail', + canActivate: [mailLoginGuard], + canActivateChild: [mailLoginGuard], children: [ + { + path: '', + redirectTo: 'INBOX', + pathMatch: 'full', + }, { path: ':mailbox', loadComponent: () => import('./mail-page.component').then(m => m.MailPageComponent), diff --git a/frontend/app/src/app/modules/mail/mail.service.ts b/frontend/app/src/app/modules/mail/mail.service.ts index 7d6a82d5..5f43499a 100644 --- a/frontend/app/src/app/modules/mail/mail.service.ts +++ b/frontend/app/src/app/modules/mail/mail.service.ts @@ -1,10 +1,12 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {Observable, map, tap, mergeMap, of, forkJoin, catchError} from 'rxjs'; +import {Observable, map, tap, mergeMap, of, forkJoin, catchError, BehaviorSubject} from 'rxjs'; import PostalMime from 'postal-mime'; import {ContentInfo, SignedData} from 'pkijs'; -import {RawEmail, Email, SignedValue, RawEmailBodyStructure, Signature} from './schema'; +import {RawEmail, Email, SignedValue, RawEmailBodyStructure, Signature, MailboxTreeRoot} from './schema'; import {z} from 'zod'; +import {MailStorageProvider} from './mail-storage.provider'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; function value(value: undefined): undefined; function value(value: T): SignedValue; @@ -14,13 +16,37 @@ function value(value: T | undefined): SignedValue | undefined { @Injectable({providedIn: 'root'}) export class MailService { - constructor(private httpClient: HttpClient) {} + isLoggedIn = this.mailStorage.credentials.pipe(map(it => it !== undefined)); + + manualSync = new BehaviorSubject(undefined); + + constructor( + private httpClient: HttpClient, + private mailStorage: MailStorageProvider, + ) { + this.mailStorage.credentials + .pipe( + takeUntilDestroyed(), + mergeMap(credentials => this.manualSync.pipe(map(() => credentials))), + mergeMap(credentials => { + if (credentials !== undefined) { + return this.listMailboxes(credentials).pipe( + mergeMap(mailboxes => this.mailStorage.setMailboxes(mailboxes)), + ); + } else { + return of(); + } + }), + ) + .subscribe(() => {}); + } private request(options: { method?: string; path?: string[]; options?: Record; responseType?: 'json' | 'arraybuffer'; + credentials?: string; }): Observable { return this.httpClient.request( options.method ?? 'GET', @@ -31,17 +57,42 @@ export class MailService { }`, { responseType: options.responseType as 'json', - headers: {authorization: `Basic ${btoa('test:123')}`}, + headers: options.credentials ? {authorization: `Basic ${options.credentials}`} : undefined, }, ); } - isLoggedIn() { - return false; + login(username: string, password: string): Observable { + const credentials = btoa(`${username}:${password}`); + return this.request({ + path: [], + options: {}, + responseType: 'json', + credentials, + }).pipe( + map(() => true), + catchError(error => { + if (error.status === 401) { + return of(false); + } else { + throw error; + } + }), + tap(success => { + if (success) { + this.mailStorage.setCredentials(credentials); + } + }), + ); } - listMailboxes(): Observable { - return this.request({}); + logout() { + this.mailStorage.setCredentials(undefined); + this.mailStorage.setMailboxes(undefined); + } + + listMailboxes(credentials: string): Observable { + return this.request({credentials}).pipe(mergeMap(it => MailboxTreeRoot.parseAsync(it))); } private listRawEmails(mailbox: string, since?: Date): Observable { diff --git a/frontend/app/src/app/modules/mail/schema.ts b/frontend/app/src/app/modules/mail/schema.ts index a5fd0a1b..df61788b 100644 --- a/frontend/app/src/app/modules/mail/schema.ts +++ b/frontend/app/src/app/modules/mail/schema.ts @@ -51,6 +51,35 @@ export const RawEmail = z.object({ export type RawEmail = z.infer; +const MailboxTreeItemBase = z.object({ + path: z.string(), + name: z.string(), + delimiter: z.string(), + flags: z.array(z.string()).or(z.object({})), + specialUse: z.optional( + z.enum(['\\All', '\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash', '\\Inbox']), + ), + listed: z.boolean(), + subscribed: z.boolean(), + disabled: z.boolean().optional(), +}); + +export type MailboxTreeItem = z.infer & { + folders?: MailboxTreeItem[]; +}; + +export const MailboxTreeItem: z.ZodType = MailboxTreeItemBase.extend({ + folders: z.optional(z.lazy(() => z.array(MailboxTreeItem))), +}); + +export const MailboxTreeRoot = z.object({ + path: z.literal('').default(''), + root: z.literal(true), + folders: z.array(MailboxTreeItem), +}); + +export type MailboxTreeRoot = z.infer; + export interface Signature { type: 'pkcs7'; valid: boolean; diff --git a/frontend/app/src/assets/i18n/de.json b/frontend/app/src/assets/i18n/de.json index 0970662e..bc5e1456 100644 --- a/frontend/app/src/assets/i18n/de.json +++ b/frontend/app/src/assets/i18n/de.json @@ -395,7 +395,16 @@ "FROM": "von", "SENDER": "Absender", "TO": "an", - "DATE": "Datum" + "DATE": "Datum", + "login": { + "TITLE": "E-Mail Login", + "LOGIN": "Login", + "PLACEHOLDER_USERNAME": "Nutzername", + "PLACEHOLDER_PASSWORD": "Passwort", + "error": { + "INVALID_CREDENTIALS": "ungültige Zugangsdaten" + } + } }, "menu": { "context": { diff --git a/frontend/app/src/assets/i18n/en.json b/frontend/app/src/assets/i18n/en.json index 57494a63..33e92eb2 100644 --- a/frontend/app/src/assets/i18n/en.json +++ b/frontend/app/src/assets/i18n/en.json @@ -395,7 +395,16 @@ "FROM": "from", "SENDER": "sender", "TO": "to", - "DATE": "date" + "DATE": "date", + "login": { + "TITLE": "email login", + "LOGIN": "login", + "PLACEHOLDER_USERNAME": "username", + "PLACEHOLDER_PASSWORD": "password", + "error": { + "INVALID_CREDENTIALS": "invalid credentials" + } + } }, "menu": { "context": { diff --git a/frontend/app/src/assets/icons.min.woff2 b/frontend/app/src/assets/icons.min.woff2 index 1ab25045..270f1d29 100644 Binary files a/frontend/app/src/assets/icons.min.woff2 and b/frontend/app/src/assets/icons.min.woff2 differ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f675cbf5..f9535f2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -330,9 +330,6 @@ importers: prettier: specifier: 3.1.1 version: 3.1.1 - swagger-jsdoc: - specifier: ^6.2.8 - version: 6.2.8(openapi-types@12.1.0) tsup: specifier: 6.7.0 version: 6.7.0(ts-node@10.9.2)(typescript@5.4.2) @@ -2723,38 +2720,6 @@ packages: tslib: 2.6.2 dev: false - /@apidevtools/json-schema-ref-parser@9.1.2: - resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} - dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.11 - call-me-maybe: 1.0.2 - js-yaml: 4.1.0 - dev: true - - /@apidevtools/openapi-schemas@2.1.0: - resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} - engines: {node: '>=10'} - dev: true - - /@apidevtools/swagger-methods@3.0.2: - resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} - dev: true - - /@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.0): - resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==} - peerDependencies: - openapi-types: '>=7' - dependencies: - '@apidevtools/json-schema-ref-parser': 9.1.2 - '@apidevtools/openapi-schemas': 2.1.0 - '@apidevtools/swagger-methods': 3.0.2 - '@jsdevtools/ono': 7.1.3 - call-me-maybe: 1.0.2 - openapi-types: 12.1.0 - z-schema: 5.0.5 - dev: true - /@awesome-cordova-plugins/calendar@6.6.0(@awesome-cordova-plugins/core@6.6.0)(rxjs@7.8.1): resolution: {integrity: sha512-NobAl4xvmq2zBeOnLI+pqRVpC66p7OpCwd3jzrQ26h8kqhr0o5wqaNcWN6WBjmgD+/AInVnLUzsziL2QpcmD7g==} peerDependencies: @@ -7103,10 +7068,6 @@ packages: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - /@jsdevtools/ono@7.1.3: - resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} - dev: true - /@leichtgewicht/ip-codec@2.0.5: resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} dev: true @@ -10036,10 +9997,6 @@ packages: set-function-length: 1.2.2 dev: true - /call-me-maybe@1.0.2: - resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} - dev: true - /callsite@1.0.0: resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} dev: true @@ -10486,11 +10443,6 @@ packages: engines: {node: '>= 6'} dev: true - /commander@6.2.0: - resolution: {integrity: sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==} - engines: {node: '>= 6'} - dev: true - /commander@6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} @@ -15655,10 +15607,6 @@ packages: resolution: {integrity: sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==} dev: true - /lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - dev: true - /lodash.ismatch@4.4.0: resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} dev: true @@ -15674,10 +15622,6 @@ packages: /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - /lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - dev: true - /lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} dev: true @@ -17411,6 +17355,7 @@ packages: /openapi-types@12.1.0: resolution: {integrity: sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA==} + dev: false /opencollective-postinstall@2.0.3: resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} @@ -20407,30 +20352,6 @@ packages: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} dev: true - /swagger-jsdoc@6.2.8(openapi-types@12.1.0): - resolution: {integrity: sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==} - engines: {node: '>=12.0.0'} - hasBin: true - dependencies: - commander: 6.2.0 - doctrine: 3.0.0 - glob: 7.1.6 - lodash.mergewith: 4.6.2 - swagger-parser: 10.0.3(openapi-types@12.1.0) - yaml: 2.0.0-1 - transitivePeerDependencies: - - openapi-types - dev: true - - /swagger-parser@10.0.3(openapi-types@12.1.0): - resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==} - engines: {node: '>=10'} - dependencies: - '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.0) - transitivePeerDependencies: - - openapi-types - dev: true - /swiper@8.4.5: resolution: {integrity: sha512-zveyEFBBv4q1sVkbJHnuH4xCtarKieavJ4SxP0QEHvdpPLJRuD7j/Xg38IVVLbp7Db6qrPsLUePvxohYx39Agw==} engines: {node: '>= 4.7.0'} @@ -21573,11 +21494,6 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true - /validator@13.12.0: - resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} - engines: {node: '>= 0.10'} - dev: true - /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -22255,11 +22171,6 @@ packages: engines: {node: '>= 6'} dev: true - /yaml@2.0.0-1: - resolution: {integrity: sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==} - engines: {node: '>= 6'} - dev: true - /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -22356,18 +22267,6 @@ packages: engines: {node: '>=12.20'} dev: true - /z-schema@5.0.5: - resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} - engines: {node: '>=8.0.0'} - hasBin: true - dependencies: - lodash.get: 4.4.2 - lodash.isequal: 4.5.0 - validator: 13.12.0 - optionalDependencies: - commander: 9.5.0 - dev: true - /zepto@1.2.0: resolution: {integrity: sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==} dev: true