diff options
Diffstat (limited to 'overlays')
20 files changed, 474 insertions, 107 deletions
diff --git a/overlays/spm/frontend/angular.json b/overlays/spm/frontend/angular.json index cc08d665..a25bfd0b 100644 --- a/overlays/spm/frontend/angular.json +++ b/overlays/spm/frontend/angular.json | |||
| @@ -54,7 +54,7 @@ | |||
| 54 | "assets": [ | 54 | "assets": [ |
| 55 | "src/assets", | 55 | "src/assets", |
| 56 | { | 56 | { |
| 57 | "glob": "@(thumb_up)/baseline.svg", | 57 | "glob": "@(add|check|delete_forever)/baseline.svg", |
| 58 | "input": "./node_modules/@material-icons/svg/svg", | 58 | "input": "./node_modules/@material-icons/svg/svg", |
| 59 | "output": "/icons/" | 59 | "output": "/icons/" |
| 60 | } | 60 | } |
diff --git a/overlays/spm/frontend/package.json b/overlays/spm/frontend/package.json index 74010466..54a301a0 100644 --- a/overlays/spm/frontend/package.json +++ b/overlays/spm/frontend/package.json | |||
| @@ -15,16 +15,21 @@ | |||
| 15 | "@angular/common": "~13.3.0", | 15 | "@angular/common": "~13.3.0", |
| 16 | "@angular/compiler": "~13.3.0", | 16 | "@angular/compiler": "~13.3.0", |
| 17 | "@angular/core": "~13.3.0", | 17 | "@angular/core": "~13.3.0", |
| 18 | "@angular/flex-layout": "^13.0.0-beta.38", | ||
| 18 | "@angular/forms": "~13.3.0", | 19 | "@angular/forms": "~13.3.0", |
| 19 | "@angular/material": "13.3.7", | 20 | "@angular/material": "13.3.7", |
| 20 | "@angular/platform-browser": "~13.3.0", | 21 | "@angular/platform-browser": "~13.3.0", |
| 21 | "@angular/platform-browser-dynamic": "~13.3.0", | 22 | "@angular/platform-browser-dynamic": "~13.3.0", |
| 22 | "@angular/router": "~13.3.0", | 23 | "@angular/router": "~13.3.0", |
| 23 | "@fontsource/roboto": "^4.5.7", | 24 | "@fontsource/roboto": "^4.5.7", |
| 25 | "@fontsource/source-sans-pro": "^4.5.10", | ||
| 24 | "@material-icons/svg": "^1.0.28", | 26 | "@material-icons/svg": "^1.0.28", |
| 27 | "@types/uuid": "^8.3.4", | ||
| 28 | "jwt-decode": "^3.1.2", | ||
| 25 | "ngx-webstorage": "9.0.0", | 29 | "ngx-webstorage": "9.0.0", |
| 26 | "rxjs": "~7.5.0", | 30 | "rxjs": "~7.5.0", |
| 27 | "tslib": "^2.3.0", | 31 | "tslib": "^2.3.0", |
| 32 | "uuid": "^8.3.2", | ||
| 28 | "zone.js": "~0.11.4" | 33 | "zone.js": "~0.11.4" |
| 29 | }, | 34 | }, |
| 30 | "devDependencies": { | 35 | "devDependencies": { |
diff --git a/overlays/spm/frontend/src/app/app-routing.module.ts b/overlays/spm/frontend/src/app/app-routing.module.ts index 2da97cea..bffe8c4d 100644 --- a/overlays/spm/frontend/src/app/app-routing.module.ts +++ b/overlays/spm/frontend/src/app/app-routing.module.ts | |||
| @@ -1,17 +1,14 @@ | |||
| 1 | import { NgModule } from '@angular/core'; | 1 | import { NgModule } from '@angular/core'; |
| 2 | import { RouterModule, Routes } from '@angular/router'; | 2 | import { RouterModule, Routes, PreloadAllModules } from '@angular/router'; |
| 3 | |||
| 4 | import { SpmComponent } from './spm/spm.component'; | ||
| 5 | import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; | ||
| 6 | 3 | ||
| 7 | const routes: Routes = [ | 4 | const routes: Routes = [ |
| 8 | { path: 'spm', component: SpmComponent }, | 5 | { path: 'spm', loadChildren: () => import('./spm/spm.module').then(m => m.SpmModule) }, |
| 9 | { path: '', redirectTo: '/spm', pathMatch: 'full' }, | 6 | { path: '', redirectTo: '/spm', pathMatch: 'full' }, |
| 10 | { path: '**', component: PageNotFoundComponent } | 7 | { path: '**', loadChildren: () => import('./page-not-found/page-not-found.module').then(m => m.PageNotFoundModule) } |
| 11 | ]; | 8 | ]; |
| 12 | 9 | ||
| 13 | @NgModule({ | 10 | @NgModule({ |
| 14 | imports: [RouterModule.forRoot(routes)], | 11 | imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })], |
| 15 | exports: [RouterModule] | 12 | exports: [RouterModule] |
| 16 | }) | 13 | }) |
| 17 | export class AppRoutingModule { } | 14 | export class AppRoutingModule { } |
diff --git a/overlays/spm/frontend/src/app/app.component.html b/overlays/spm/frontend/src/app/app.component.html index f9b5b8fc..1a2f88b0 100644 --- a/overlays/spm/frontend/src/app/app.component.html +++ b/overlays/spm/frontend/src/app/app.component.html | |||
| @@ -1,7 +1,21 @@ | |||
| 1 | <mat-toolbar color="primary"> | 1 | <div gdAreas="nav | content" gdRows="min-content 1fr" style="min-height: 100vh"> |
| 2 | <div> | 2 | <mat-toolbar color="primary" gdArea="nav" fxLayout="flex" fxLayoutGap="16px"> |
| 3 | <a mat-button routerLink="/spm" routerLinkActive="active">spm</a> | 3 | <div> |
| 4 | </div> | 4 | <span class="mono" [innerHTML]="apiDomain$ | async"></span> |
| 5 | </mat-toolbar> | 5 | </div> |
| 6 | <!-- The routed views render in the <router-outlet> --> | 6 | <div> |
| 7 | <router-outlet></router-outlet> | 7 | <a mat-button routerLink="/spm" routerLinkActive="active">spm</a> |
| 8 | </div> | ||
| 9 | </mat-toolbar> | ||
| 10 | <!-- The routed views render in the <router-outlet> --> | ||
| 11 | <ng-template #router_loading_indicator> | ||
| 12 | <div gdArea="content" fxLayout="column" fxLayoutAlign="center stretch"> | ||
| 13 | <div fxFlex="none" fxFlexAlign="center"> | ||
| 14 | <mat-spinner></mat-spinner> | ||
| 15 | </div> | ||
| 16 | </div> | ||
| 17 | </ng-template> | ||
| 18 | <div class="content" gdArea="content" *ngIf="!(routerLoading$ | async); else router_loading_indicator"> | ||
| 19 | <router-outlet></router-outlet> | ||
| 20 | </div> | ||
| 21 | </div> | ||
diff --git a/overlays/spm/frontend/src/app/app.component.sass b/overlays/spm/frontend/src/app/app.component.sass index e69de29b..4732ea49 100644 --- a/overlays/spm/frontend/src/app/app.component.sass +++ b/overlays/spm/frontend/src/app/app.component.sass | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | .content | ||
| 2 | padding: 16px | ||
diff --git a/overlays/spm/frontend/src/app/app.component.ts b/overlays/spm/frontend/src/app/app.component.ts index 7c231203..11c0f772 100644 --- a/overlays/spm/frontend/src/app/app.component.ts +++ b/overlays/spm/frontend/src/app/app.component.ts | |||
| @@ -1,6 +1,18 @@ | |||
| 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; | 1 | import { Component, OnInit, OnDestroy, Inject, Renderer2, RendererFactory2 } from '@angular/core'; |
| 2 | import { DOCUMENT } from '@angular/common'; | ||
| 3 | import { MediaMatcher } from '@angular/cdk/layout'; | ||
| 4 | import { LocalStorage } from 'ngx-webstorage'; | ||
| 5 | import { BehaviorSubject, Subscription, Subject } from 'rxjs'; | ||
| 6 | import { Router, RouterEvent, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router'; | ||
| 7 | import { HttpClient, HttpHeaders } from '@angular/common/http'; | ||
| 2 | 8 | ||
| 3 | import { ColorSchemeService } from './color-scheme.service'; | 9 | import { map, filter } from 'rxjs/operators'; |
| 10 | |||
| 11 | type ColorScheme = 'dark' | 'light'; | ||
| 12 | |||
| 13 | export interface DomainResponse { | ||
| 14 | domain: string; | ||
| 15 | } | ||
| 4 | 16 | ||
| 5 | @Component({ | 17 | @Component({ |
| 6 | selector: 'app-root', | 18 | selector: 'app-root', |
| @@ -8,14 +20,81 @@ import { ColorSchemeService } from './color-scheme.service'; | |||
| 8 | styleUrls: ['./app.component.sass'] | 20 | styleUrls: ['./app.component.sass'] |
| 9 | }) | 21 | }) |
| 10 | export class AppComponent implements OnInit, OnDestroy { | 22 | export class AppComponent implements OnInit, OnDestroy { |
| 23 | private renderer: Renderer2; | ||
| 24 | colorSchemeMatcher: MediaQueryList; | ||
| 25 | |||
| 11 | title = 'spm-frontend'; | 26 | title = 'spm-frontend'; |
| 12 | 27 | ||
| 13 | constructor(private colorSchemeService: ColorSchemeService) {} | 28 | public activeColorScheme$: BehaviorSubject<ColorScheme> = new BehaviorSubject<ColorScheme>('dark'); |
| 29 | |||
| 30 | @LocalStorage('prefers-color-scheme') | ||
| 31 | public colorSchemeOverride?: ColorScheme; | ||
| 32 | |||
| 33 | colorSchemeSubscription?: Subscription; | ||
| 34 | |||
| 35 | router: Router; | ||
| 36 | public routerLoading$: Subject<boolean> = new Subject<boolean>(); | ||
| 37 | routerEventSubscription?: Subscription; | ||
| 38 | |||
| 39 | http: HttpClient; | ||
| 40 | apiDomain$: Subject<string> = new Subject<string>(); | ||
| 41 | |||
| 42 | constructor( rendererFactory: RendererFactory2, | ||
| 43 | mediaMatcher: MediaMatcher, | ||
| 44 | @Inject(DOCUMENT) private document: Document, | ||
| 45 | router: Router, | ||
| 46 | http: HttpClient, | ||
| 47 | ) { | ||
| 48 | this.renderer = rendererFactory.createRenderer(null, null); | ||
| 49 | this.colorSchemeMatcher = mediaMatcher.matchMedia('(prefers-color-scheme: dark)'); | ||
| 50 | this.document = document; | ||
| 51 | |||
| 52 | this.colorSchemeListener = this.colorSchemeListener.bind(this); | ||
| 53 | this.colorSchemeMatcher.addEventListener('change', this.colorSchemeListener); | ||
| 54 | this.activeColorScheme$.next(this.getColorScheme()); | ||
| 55 | |||
| 56 | this.router = router; | ||
| 57 | this.http = http; | ||
| 58 | } | ||
| 59 | |||
| 60 | private colorSchemeListener(event: { matches: boolean; }) { | ||
| 61 | this.activeColorScheme$.next(this.getColorScheme(event)); | ||
| 62 | } | ||
| 63 | |||
| 64 | private getColorScheme(event?: { matches: boolean; }): ColorScheme { | ||
| 65 | if (this.colorSchemeOverride) { | ||
| 66 | return this.colorSchemeOverride; | ||
| 67 | } else if (event) { | ||
| 68 | return event.matches ? 'dark' : 'light'; | ||
| 69 | } else { | ||
| 70 | return this.colorSchemeMatcher.matches ? 'dark' : 'light'; | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | updateColorScheme(scheme: ColorScheme) { | ||
| 75 | this.colorSchemeOverride = scheme; | ||
| 76 | this.activeColorScheme$.next(scheme); | ||
| 77 | } | ||
| 14 | 78 | ||
| 15 | ngOnInit() { | 79 | ngOnInit() { |
| 16 | this.colorSchemeService.init(); | 80 | this.colorSchemeSubscription = this.activeColorScheme$.subscribe(scheme => { this.renderer.setAttribute(this.document.body, 'data-color-scheme', scheme); }); |
| 81 | |||
| 82 | this.routerEventSubscription = this.router.events.pipe( | ||
| 83 | filter(event => | ||
| 84 | event instanceof NavigationStart || | ||
| 85 | event instanceof NavigationEnd || | ||
| 86 | event instanceof NavigationCancel || | ||
| 87 | event instanceof NavigationError | ||
| 88 | ), | ||
| 89 | map(event => event instanceof NavigationStart) | ||
| 90 | ).subscribe(this.routerLoading$); | ||
| 91 | |||
| 92 | this.http.get<DomainResponse>('/domain', { headers: new HttpHeaders({ 'Accept': 'application/json' }) }).pipe(map((resp: DomainResponse) => resp.domain)).subscribe(this.apiDomain$); | ||
| 17 | } | 93 | } |
| 18 | ngOnDestroy() { | 94 | ngOnDestroy() { |
| 19 | this.colorSchemeService.destroy(); | 95 | this.colorSchemeMatcher.removeEventListener('change', this.colorSchemeListener); |
| 96 | |||
| 97 | this.colorSchemeSubscription?.unsubscribe(); | ||
| 98 | this.routerEventSubscription?.unsubscribe(); | ||
| 20 | } | 99 | } |
| 21 | } | 100 | } |
diff --git a/overlays/spm/frontend/src/app/app.module.ts b/overlays/spm/frontend/src/app/app.module.ts index 914f73a2..98e5106e 100644 --- a/overlays/spm/frontend/src/app/app.module.ts +++ b/overlays/spm/frontend/src/app/app.module.ts | |||
| @@ -1,30 +1,30 @@ | |||
| 1 | import { NgModule } from '@angular/core'; | 1 | import { NgModule } from '@angular/core'; |
| 2 | import { BrowserModule } from '@angular/platform-browser'; | 2 | import { BrowserModule } from '@angular/platform-browser'; |
| 3 | import { NgxWebstorageModule } from 'ngx-webstorage'; | ||
| 3 | 4 | ||
| 4 | import { AppRoutingModule } from './app-routing.module'; | 5 | import { AppRoutingModule } from './app-routing.module'; |
| 5 | import { AppComponent } from './app.component'; | 6 | import { AppComponent } from './app.component'; |
| 6 | import { SpmComponent } from './spm/spm.component'; | ||
| 7 | import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; | ||
| 8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; | 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; |
| 9 | import { MatIconModule } from '@angular/material/icon'; | ||
| 10 | import { HttpClientModule } from "@angular/common/http"; | 8 | import { HttpClientModule } from "@angular/common/http"; |
| 11 | import { MatToolbarModule } from '@angular/material/toolbar'; | 9 | import { MatToolbarModule } from '@angular/material/toolbar'; |
| 12 | import { MatButtonModule } from '@angular/material/button'; | 10 | import { MatButtonModule } from '@angular/material/button'; |
| 11 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; | ||
| 12 | import { FlexLayoutModule } from "@angular/flex-layout"; | ||
| 13 | 13 | ||
| 14 | @NgModule({ | 14 | @NgModule({ |
| 15 | declarations: [ | 15 | declarations: [ |
| 16 | AppComponent, | 16 | AppComponent, |
| 17 | SpmComponent, | ||
| 18 | PageNotFoundComponent | ||
| 19 | ], | 17 | ], |
| 20 | imports: [ | 18 | imports: [ |
| 21 | BrowserModule, | 19 | BrowserModule, |
| 20 | NgxWebstorageModule.forRoot(), | ||
| 22 | AppRoutingModule, | 21 | AppRoutingModule, |
| 23 | BrowserAnimationsModule, | 22 | BrowserAnimationsModule, |
| 24 | MatIconModule, | ||
| 25 | HttpClientModule, | 23 | HttpClientModule, |
| 26 | MatToolbarModule, | 24 | MatToolbarModule, |
| 27 | MatButtonModule | 25 | MatButtonModule, |
| 26 | MatProgressSpinnerModule, | ||
| 27 | FlexLayoutModule, | ||
| 28 | ], | 28 | ], |
| 29 | providers: [], | 29 | providers: [], |
| 30 | bootstrap: [AppComponent] | 30 | bootstrap: [AppComponent] |
diff --git a/overlays/spm/frontend/src/app/color-scheme.service.ts b/overlays/spm/frontend/src/app/color-scheme.service.ts deleted file mode 100644 index c56e9c23..00000000 --- a/overlays/spm/frontend/src/app/color-scheme.service.ts +++ /dev/null | |||
| @@ -1,62 +0,0 @@ | |||
| 1 | import { Injectable, Renderer2, RendererFactory2 } from '@angular/core'; | ||
| 2 | import { MediaMatcher } from '@angular/cdk/layout'; | ||
| 3 | import { LocalStorage } from 'ngx-webstorage'; | ||
| 4 | |||
| 5 | type ColorScheme = "dark" | "light"; | ||
| 6 | |||
| 7 | @Injectable({ | ||
| 8 | providedIn: 'root' | ||
| 9 | }) | ||
| 10 | export class ColorSchemeService { | ||
| 11 | private renderer: Renderer2; | ||
| 12 | matcher!: MediaQueryList; | ||
| 13 | |||
| 14 | @LocalStorage('prefers-color-scheme') | ||
| 15 | public colorSchemeOverride: ColorScheme; | ||
| 16 | |||
| 17 | public activeColorScheme: BehaviorSubject<ColorScheme> = new BehaviorSubject("light"); | ||
| 18 | |||
| 19 | constructor(rendererFactory: RendererFactory2, | ||
| 20 | mediaMatcher: MediaMatcher | ||
| 21 | ) { | ||
| 22 | // Create new renderer from renderFactory, to make it possible to use renderer2 in a service | ||
| 23 | this.renderer = rendererFactory.createRenderer(null, null); | ||
| 24 | this.matcher = mediaMatcher.matchMedia('(prefers-color-scheme: dark)'); | ||
| 25 | |||
| 26 | this._listener = this._listener.bind(this); | ||
| 27 | } | ||
| 28 | |||
| 29 | init() { | ||
| 30 | this.matcher.addEventListener('change', this._listener); | ||
| 31 | this.load(); | ||
| 32 | } | ||
| 33 | |||
| 34 | _listener(event: { matches: any; }) { | ||
| 35 | this.activeColorScheme.next(event.matches ? 'dark' : 'light'); | ||
| 36 | } | ||
| 37 | |||
| 38 | destroy() { | ||
| 39 | this.matcher.removeEventListener('change', this._listener); | ||
| 40 | } | ||
| 41 | |||
| 42 | _getColorScheme() { | ||
| 43 | if (this.colorSchemeOverride) { | ||
| 44 | this.colorSchemeOverride | ||
| 45 | } else { | ||
| 46 | this.matcher.matches ? 'dark' : 'light' | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | load() { | ||
| 51 | this.activeColorScheme.next(this._getColorScheme()); | ||
| 52 | } | ||
| 53 | |||
| 54 | update(scheme: ColorScheme) { | ||
| 55 | this.colorSchemeOverride = scheme; | ||
| 56 | this.activeColorScheme.next(scheme); | ||
| 57 | } | ||
| 58 | |||
| 59 | currentActive() { | ||
| 60 | return this.colorScheme; | ||
| 61 | } | ||
| 62 | } | ||
diff --git a/overlays/spm/frontend/src/app/page-not-found/page-not-found.module.ts b/overlays/spm/frontend/src/app/page-not-found/page-not-found.module.ts new file mode 100644 index 00000000..b9d8945c --- /dev/null +++ b/overlays/spm/frontend/src/app/page-not-found/page-not-found.module.ts | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | import { NgModule } from '@angular/core'; | ||
| 2 | import { PageNotFoundComponent } from './page-not-found.component'; | ||
| 3 | import { RouterModule } from '@angular/router'; | ||
| 4 | |||
| 5 | @NgModule({ | ||
| 6 | declarations: [PageNotFoundComponent], | ||
| 7 | exports: [PageNotFoundComponent], | ||
| 8 | imports: [ | ||
| 9 | RouterModule.forChild([{ | ||
| 10 | path: '', | ||
| 11 | pathMatch: 'full', | ||
| 12 | component: PageNotFoundComponent | ||
| 13 | }]), | ||
| 14 | ] | ||
| 15 | }) | ||
| 16 | export class PageNotFoundModule {} | ||
diff --git a/overlays/spm/frontend/src/app/spm/spm.component.html b/overlays/spm/frontend/src/app/spm/spm.component.html index d1339d95..5d0e625a 100644 --- a/overlays/spm/frontend/src/app/spm/spm.component.html +++ b/overlays/spm/frontend/src/app/spm/spm.component.html | |||
| @@ -1,3 +1,33 @@ | |||
| 1 | <p>spm works!</p> | 1 | <div id="mail-panel-container" fxLayout="row wrap" style="gap: 16px"> |
| 2 | <ng-template ngFor [ngForOf]="spmMails$ | async | keyvalue: asIsOrder" let-entry> | ||
| 3 | <mat-card> | ||
| 4 | <mat-card-title class="mono" *ngIf="entry.value.state !== 'loading'">{{entry.value.local}}</mat-card-title> | ||
| 5 | <mat-card-subtitle class="mono" *ngIf="entry.value.state !== 'loading'">@{{entry.value.domain}}</mat-card-subtitle> | ||
| 6 | <mat-card-content *ngIf="entry.value.state === 'loading'"> | ||
| 7 | <mat-spinner style="margin: auto"></mat-spinner> | ||
| 8 | </mat-card-content> | ||
| 9 | <mat-card-actions> | ||
| 10 | <button mat-raised-button color="primary" (click)="claim(entry.key)" [disabled]="entry.value.state === 'loaded' || entry.value.state === 'claiming' ? null : true"> | ||
| 11 | <mat-icon aria-hidden="false" aria-label="Claim" svgIcon="check"></mat-icon> | ||
| 12 | <span *ngIf="entry.value.state !== 'claimed'">Claim</span> | ||
| 13 | <span *ngIf="entry.value.state === 'claimed'">Claimed</span> | ||
| 14 | </button> | ||
| 15 | <button mat-stroked-button color="warn" (click)="forget(entry.key)"> | ||
| 16 | <mat-icon aria-hidden="false" aria-label="Forget" svgIcon="delete_forever"></mat-icon> | ||
| 17 | <span *ngIf="entry.value.state !== 'claimed'">Forget</span> | ||
| 18 | <span *ngIf="entry.value.state === 'claimed'">Hide</span> | ||
| 19 | </button> | ||
| 20 | </mat-card-actions> | ||
| 21 | <mat-card-footer *ngIf="entry.value.state === 'loaded' && entry.value.percent_expiration"> | ||
| 22 | <mat-progress-bar mode="determinate" [value]="entry.value.percent_expiration"></mat-progress-bar> | ||
| 23 | </mat-card-footer> | ||
| 24 | <mat-card-footer *ngIf="entry.value.state === 'claiming'"> | ||
| 25 | <mat-progress-bar mode="indeterminate"></mat-progress-bar> | ||
| 26 | </mat-card-footer> | ||
| 27 | </mat-card> | ||
| 28 | </ng-template> | ||
| 29 | </div> | ||
| 2 | 30 | ||
| 3 | <mat-icon aria-hidden="false" aria-label="Thumbs up" svgIcon="thumb_up"></mat-icon> | 31 | <button id="add-button" mat-fab color="accent" (click)="add()"> |
| 32 | <mat-icon aria-hidden="false" aria-label="Add" svgIcon="add"></mat-icon> | ||
| 33 | </button> | ||
diff --git a/overlays/spm/frontend/src/app/spm/spm.component.sass b/overlays/spm/frontend/src/app/spm/spm.component.sass index e69de29b..bda4dd4a 100644 --- a/overlays/spm/frontend/src/app/spm/spm.component.sass +++ b/overlays/spm/frontend/src/app/spm/spm.component.sass | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | #add-button | ||
| 2 | position: fixed | ||
| 3 | bottom: 16px | ||
| 4 | right: 16px | ||
| 5 | |||
| 6 | #mail-panel-container | ||
| 7 | margin-bottom: 88px | ||
| 8 | |||
| 9 | .mat-card | ||
| 10 | overflow: hidden | ||
| 11 | |||
| 12 | .mat-card-footer | ||
| 13 | margin: -16px | ||
diff --git a/overlays/spm/frontend/src/app/spm/spm.component.ts b/overlays/spm/frontend/src/app/spm/spm.component.ts index 7d172052..f0655b55 100644 --- a/overlays/spm/frontend/src/app/spm/spm.component.ts +++ b/overlays/spm/frontend/src/app/spm/spm.component.ts | |||
| @@ -1,24 +1,193 @@ | |||
| 1 | import { Component, OnInit } from '@angular/core'; | 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; |
| 2 | import { MatIconRegistry, MatIconModule } from '@angular/material/icon'; | 2 | import { MatIconRegistry, MatIconModule } from '@angular/material/icon'; |
| 3 | import { DomSanitizer } from "@angular/platform-browser"; | 3 | import { DomSanitizer } from "@angular/platform-browser"; |
| 4 | import { BehaviorSubject, interval, Subscription, catchError, throwError } from 'rxjs'; | ||
| 5 | import { HttpClient, HttpHeaders, HttpErrorResponse, HttpResponse } from '@angular/common/http'; | ||
| 6 | |||
| 7 | import { v4 as uuidv4 } from 'uuid'; | ||
| 8 | import jwtDecode, { JwtPayload } from "jwt-decode"; | ||
| 9 | |||
| 10 | import { map, withLatestFrom } from 'rxjs/operators'; | ||
| 11 | |||
| 12 | export interface LoadingSpmMail { | ||
| 13 | state: "loading"; | ||
| 14 | } | ||
| 15 | |||
| 16 | export interface LoadedSpmMail { | ||
| 17 | state: "loaded" | "claiming"; | ||
| 18 | local: string; | ||
| 19 | domain: string; | ||
| 20 | jwt: string; | ||
| 21 | start?: Date; | ||
| 22 | expiration?: Date; | ||
| 23 | percent_expiration?: number; | ||
| 24 | } | ||
| 25 | |||
| 26 | export interface ExpiredSpmMail { | ||
| 27 | state: "expired"; | ||
| 28 | local: string; | ||
| 29 | domain: string; | ||
| 30 | } | ||
| 31 | |||
| 32 | export interface ClaimedSpmMail { | ||
| 33 | state: "claimed"; | ||
| 34 | local: string; | ||
| 35 | domain: string; | ||
| 36 | } | ||
| 37 | |||
| 38 | export interface SpmJwtUnregisteredPayload { | ||
| 39 | local: string; | ||
| 40 | } | ||
| 41 | |||
| 42 | type SpmJwtPayload = JwtPayload & SpmJwtUnregisteredPayload; | ||
| 43 | |||
| 44 | export type SpmMail = LoadingSpmMail | LoadedSpmMail | ClaimedSpmMail | ExpiredSpmMail; | ||
| 45 | |||
| 46 | function percentExpiration(start: Date, exp: Date, now?: Date) { | ||
| 47 | if (!now) { | ||
| 48 | now = new Date(); | ||
| 49 | } | ||
| 50 | |||
| 51 | if (now < start) { | ||
| 52 | return 100; | ||
| 53 | } else if (exp < now) { | ||
| 54 | return 0; | ||
| 55 | } else { | ||
| 56 | const length = exp.getTime() - start.getTime(); | ||
| 57 | const diff = exp.getTime() - now.getTime(); | ||
| 58 | return 100 * (diff / length); | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | function updateSpmExpiration(value: SpmMail): SpmMail { | ||
| 63 | if (value.state === 'loaded') { | ||
| 64 | const percent_expiration = value.expiration && value.start ? percentExpiration(value.start, value.expiration) : undefined; | ||
| 65 | if (percent_expiration !== undefined && percent_expiration <= 0) { | ||
| 66 | return {state: "expired", local: value.local, domain: value.domain}; | ||
| 67 | } else { | ||
| 68 | return {...value, percent_expiration: percent_expiration} | ||
| 69 | } | ||
| 70 | } else { | ||
| 71 | return value; | ||
| 72 | } | ||
| 73 | } | ||
| 4 | 74 | ||
| 5 | @Component({ | 75 | @Component({ |
| 6 | selector: 'app-spm', | 76 | selector: 'app-spm', |
| 7 | templateUrl: './spm.component.html', | 77 | templateUrl: './spm.component.html', |
| 8 | styleUrls: ['./spm.component.sass'] | 78 | styleUrls: ['./spm.component.sass'] |
| 9 | }) | 79 | }) |
| 10 | export class SpmComponent implements OnInit { | 80 | export class SpmComponent implements OnInit, OnDestroy { |
| 81 | spmMails$: BehaviorSubject<Map<string, SpmMail>> = new BehaviorSubject(new Map()); | ||
| 82 | http: HttpClient; | ||
| 83 | |||
| 84 | intervalSubscription?: Subscription; | ||
| 11 | 85 | ||
| 12 | constructor(private matIconRegistry: MatIconRegistry, | 86 | constructor(private matIconRegistry: MatIconRegistry, |
| 13 | private domSanitizer: DomSanitizer | 87 | private domSanitizer: DomSanitizer, |
| 88 | http: HttpClient, | ||
| 14 | ) { | 89 | ) { |
| 15 | this.matIconRegistry.addSvgIcon( | 90 | this.matIconRegistry.addSvgIcon( |
| 16 | `thumb_up`, | 91 | `add`, |
| 17 | this.domSanitizer.bypassSecurityTrustResourceUrl(`icons/thumb_up/baseline.svg`) | 92 | this.domSanitizer.bypassSecurityTrustResourceUrl(`icons/add/baseline.svg`) |
| 93 | ); | ||
| 94 | this.matIconRegistry.addSvgIcon( | ||
| 95 | `check`, | ||
| 96 | this.domSanitizer.bypassSecurityTrustResourceUrl(`icons/check/baseline.svg`) | ||
| 97 | ); | ||
| 98 | this.matIconRegistry.addSvgIcon( | ||
| 99 | `delete_forever`, | ||
| 100 | this.domSanitizer.bypassSecurityTrustResourceUrl(`icons/delete_forever/baseline.svg`) | ||
| 18 | ); | 101 | ); |
| 102 | |||
| 103 | this.http = http; | ||
| 104 | } | ||
| 105 | |||
| 106 | add() { | ||
| 107 | const ident = uuidv4(); | ||
| 108 | const curr: Map<string, SpmMail> = this.spmMails$.getValue(); | ||
| 109 | curr.set(ident, { state: 'loading' }); | ||
| 110 | this.spmMails$.next(curr); | ||
| 111 | this.http.get('/spm/generate', { headers: new HttpHeaders({ 'Accept': 'text/plain' }), observe: 'body' as const, responseType: 'text' as const }).subscribe(payload => this.load(ident, payload)) | ||
| 112 | } | ||
| 113 | |||
| 114 | load(k: string, encoded: string) { | ||
| 115 | const payload = jwtDecode<SpmJwtPayload>(encoded); | ||
| 116 | if (typeof payload.aud === 'string') { | ||
| 117 | const curr: Map<string, SpmMail> = this.spmMails$.getValue(); | ||
| 118 | curr.set(k, { | ||
| 119 | state: 'loaded', | ||
| 120 | local: payload.local, | ||
| 121 | domain: payload.aud, | ||
| 122 | jwt: encoded, | ||
| 123 | expiration: payload.exp ? new Date(1000 * payload.exp) : undefined, | ||
| 124 | start: payload.nbf ? new Date(1000 * payload.nbf) : undefined, | ||
| 125 | percent_expiration: payload.exp && payload.nbf ? percentExpiration(new Date(1000 * payload.nbf), new Date(1000 * payload.exp)) : undefined | ||
| 126 | }); | ||
| 127 | this.spmMails$.next(curr); | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | handleClaimError(k: string, error: HttpErrorResponse) { | ||
| 132 | this.setState(k, 'loaded'); | ||
| 133 | |||
| 134 | return throwError(() => new Error('Claiming failed.')); | ||
| 135 | } | ||
| 136 | |||
| 137 | claim(k: string) { | ||
| 138 | const val = this.spmMails$.getValue().get(k); | ||
| 139 | if (!val || val.state !== 'loaded') | ||
| 140 | return; | ||
| 141 | |||
| 142 | this.setState(k, 'claiming'); | ||
| 143 | |||
| 144 | this.http.post('/spm/claim', val.jwt, { observe: 'response' as const, headers: new HttpHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }) }).pipe(catchError(error => this.handleClaimError(k, error))).subscribe(response => this.claimed(k, response)) | ||
| 19 | } | 145 | } |
| 20 | 146 | ||
| 21 | ngOnInit(): void { | 147 | setState(k: string, state: 'loaded' | 'claiming') { |
| 148 | const curr: Map<string, SpmMail> = this.spmMails$.getValue(); | ||
| 149 | const val = curr.get(k); | ||
| 150 | if (!val || (val.state !== 'loaded' && val.state !== 'claiming')) | ||
| 151 | return; | ||
| 152 | |||
| 153 | curr.set(k, {...val, state: state}); | ||
| 154 | this.spmMails$.next(curr); | ||
| 155 | } | ||
| 156 | |||
| 157 | claimed(k: string, response: HttpResponse<any>) { | ||
| 158 | if (response.status !== 204) { | ||
| 159 | this.setState(k, 'loaded'); | ||
| 160 | return; | ||
| 161 | } | ||
| 162 | |||
| 163 | const curr: Map<string, SpmMail> = this.spmMails$.getValue(); | ||
| 164 | const val = curr.get(k); | ||
| 165 | if (val && (val.state === 'claiming' || val.state === 'loaded')) { | ||
| 166 | curr.set(k, {state: "claimed", local: val.local, domain: val.domain}); | ||
| 167 | } | ||
| 168 | this.spmMails$.next(curr); | ||
| 22 | } | 169 | } |
| 23 | 170 | ||
| 171 | forget(k: string) { | ||
| 172 | const curr: Map<string, SpmMail> = this.spmMails$.getValue(); | ||
| 173 | curr.delete(k); | ||
| 174 | this.spmMails$.next(curr); | ||
| 175 | } | ||
| 176 | |||
| 177 | stepSpmMails(curr: Map<string, SpmMail>) { | ||
| 178 | curr.forEach((value, key) => curr.set(key, updateSpmExpiration(value))); | ||
| 179 | this.spmMails$.next(curr); | ||
| 180 | } | ||
| 181 | |||
| 182 | ngOnInit() { | ||
| 183 | this.intervalSubscription = interval(1000).pipe(withLatestFrom(this.spmMails$)).subscribe(([_, mails]: [number, Map<string, SpmMail>]) => this.stepSpmMails(mails)); | ||
| 184 | } | ||
| 185 | |||
| 186 | ngOnDestroy() { | ||
| 187 | this.intervalSubscription?.unsubscribe(); | ||
| 188 | } | ||
| 189 | |||
| 190 | asIsOrder(a: any, b: any) { | ||
| 191 | return -1; | ||
| 192 | } | ||
| 24 | } | 193 | } |
diff --git a/overlays/spm/frontend/src/app/spm/spm.module.ts b/overlays/spm/frontend/src/app/spm/spm.module.ts new file mode 100644 index 00000000..f51e6e98 --- /dev/null +++ b/overlays/spm/frontend/src/app/spm/spm.module.ts | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | import { NgModule } from '@angular/core'; | ||
| 2 | import { CommonModule } from '@angular/common'; | ||
| 3 | import { SpmComponent } from './spm.component'; | ||
| 4 | import { RouterModule } from '@angular/router'; | ||
| 5 | |||
| 6 | import { MatIconModule } from '@angular/material/icon'; | ||
| 7 | import { MatButtonModule } from '@angular/material/button'; | ||
| 8 | import { MatCardModule } from '@angular/material/card'; | ||
| 9 | import { FlexLayoutModule } from "@angular/flex-layout"; | ||
| 10 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; | ||
| 11 | import { MatProgressBarModule } from '@angular/material/progress-bar'; | ||
| 12 | |||
| 13 | @NgModule({ | ||
| 14 | declarations: [SpmComponent], | ||
| 15 | exports: [SpmComponent], | ||
| 16 | imports: [ | ||
| 17 | RouterModule.forChild([{ | ||
| 18 | path: '', | ||
| 19 | pathMatch: 'full', | ||
| 20 | component: SpmComponent | ||
| 21 | }]), | ||
| 22 | MatIconModule, | ||
| 23 | MatButtonModule, | ||
| 24 | MatCardModule, | ||
| 25 | CommonModule, | ||
| 26 | FlexLayoutModule, | ||
| 27 | MatProgressSpinnerModule, | ||
| 28 | MatProgressBarModule, | ||
| 29 | ] | ||
| 30 | }) | ||
| 31 | export class SpmModule {} | ||
diff --git a/overlays/spm/frontend/src/custom-theme.scss b/overlays/spm/frontend/src/custom-theme.scss index 5c2d48aa..36369bd5 100644 --- a/overlays/spm/frontend/src/custom-theme.scss +++ b/overlays/spm/frontend/src/custom-theme.scss | |||
| @@ -49,3 +49,5 @@ $spm-frontend-dark-theme: mat.define-dark-theme(( | |||
| 49 | 49 | ||
| 50 | html, body { height: 100%; } | 50 | html, body { height: 100%; } |
| 51 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } | 51 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } |
| 52 | |||
| 53 | .mono { font-family: "Source Sans Pro", monospace; } | ||
diff --git a/overlays/spm/frontend/src/styles.sass b/overlays/spm/frontend/src/styles.sass index 4f2cb82a..4ffe0b36 100644 --- a/overlays/spm/frontend/src/styles.sass +++ b/overlays/spm/frontend/src/styles.sass | |||
| @@ -1,2 +1,3 @@ | |||
| 1 | /* You can add global styles to this file, and also import other style files */ | 1 | /* You can add global styles to this file, and also import other style files */ |
| 2 | @import @fontsource/roboto | 2 | @import @fontsource/roboto |
| 3 | @import @fontsource/source-sans-pro | ||
diff --git a/overlays/spm/frontend/yarn-project.nix b/overlays/spm/frontend/yarn-project.nix index a070e3f2..07017812 100644 --- a/overlays/spm/frontend/yarn-project.nix +++ b/overlays/spm/frontend/yarn-project.nix | |||
| @@ -120,17 +120,21 @@ let | |||
| 120 | 120 | ||
| 121 | cacheEntries = { | 121 | cacheEntries = { |
| 122 | "@fontsource/roboto@npm:4.5.7" = { filename = "@fontsource-roboto-npm-4.5.7-315fd54028-3f9afc7f4c.zip"; sha512 = "3f9afc7f4c77e517e804e65823db5aea2513e8352fbc26cf7ea8b8b11e7dc6eff48d7a2855ed71e3a6b727446b2bf137629e2389fe3d676be45d95dd9992b558"; }; | 122 | "@fontsource/roboto@npm:4.5.7" = { filename = "@fontsource-roboto-npm-4.5.7-315fd54028-3f9afc7f4c.zip"; sha512 = "3f9afc7f4c77e517e804e65823db5aea2513e8352fbc26cf7ea8b8b11e7dc6eff48d7a2855ed71e3a6b727446b2bf137629e2389fe3d676be45d95dd9992b558"; }; |
| 123 | "@fontsource/source-sans-pro@npm:4.5.10" = { filename = "@fontsource-source-sans-pro-npm-4.5.10-520cde2259-d0b786cdff.zip"; sha512 = "d0b786cdff56e2b9f0dbb557473342ac95bc2021eea57097a29244936554a1f452b1ba5b053f11f2f3bcf58ba77823ac1e2a3ff8aba8c4618026a131a025a86d"; }; | ||
| 123 | "@material-icons/svg@npm:1.0.28" = { filename = "@material-icons-svg-npm-1.0.28-205466118b-c40b15422b.zip"; sha512 = "c40b15422b0d74ef9bb6ac14a8e7669a435d12bfcc8ab2fb7c986a95a37dad1abaac6e98d52b195b7643f982f7e5f21413c5c730ba5e3c810f0c04f1da068af7"; }; | 124 | "@material-icons/svg@npm:1.0.28" = { filename = "@material-icons-svg-npm-1.0.28-205466118b-c40b15422b.zip"; sha512 = "c40b15422b0d74ef9bb6ac14a8e7669a435d12bfcc8ab2fb7c986a95a37dad1abaac6e98d52b195b7643f982f7e5f21413c5c730ba5e3c810f0c04f1da068af7"; }; |
| 124 | "@types/jasmine@npm:3.10.6" = { filename = "@types-jasmine-npm-3.10.6-bc64f97df3-dff2c26a9e.zip"; sha512 = "dff2c26a9ecbc8198d2f5bf1860275b0b323c80db772c2417d7217afa28bd05fee4b98ab3673dcb18d859f2cd0e084b0dcbd33629fcbe6945b08a72f8e5c36d2"; }; | 125 | "@types/jasmine@npm:3.10.6" = { filename = "@types-jasmine-npm-3.10.6-bc64f97df3-dff2c26a9e.zip"; sha512 = "dff2c26a9ecbc8198d2f5bf1860275b0b323c80db772c2417d7217afa28bd05fee4b98ab3673dcb18d859f2cd0e084b0dcbd33629fcbe6945b08a72f8e5c36d2"; }; |
| 125 | "@types/node@npm:12.20.52" = { filename = "@types-node-npm-12.20.52-be1a44f9b8-b720ee939a.zip"; sha512 = "b720ee939a43f1427baf1606deee97e50892ff482bf3d64ab8a1eda3a50802a80866a232aeab337621f6ea4fdc4515147d7829337653e9e99d97c3aaeec3615d"; }; | 126 | "@types/node@npm:12.20.52" = { filename = "@types-node-npm-12.20.52-be1a44f9b8-b720ee939a.zip"; sha512 = "b720ee939a43f1427baf1606deee97e50892ff482bf3d64ab8a1eda3a50802a80866a232aeab337621f6ea4fdc4515147d7829337653e9e99d97c3aaeec3615d"; }; |
| 127 | "@types/uuid@npm:8.3.4" = { filename = "@types-uuid-npm-8.3.4-7547f4402c-6f11f3ff70.zip"; sha512 = "6f11f3ff70f30210edaa8071422d405e9c1d4e53abbe50fdce365150d3c698fe7bbff65c1e71ae080cbfb8fded860dbb5e174da96fdbbdfcaa3fb3daa474d20f"; }; | ||
| 126 | "jasmine-core@npm:4.0.1" = { filename = "jasmine-core-npm-4.0.1-e72087e57c-f097bf2c68.zip"; sha512 = "f097bf2c681acb55e03fb56a1569a9dfa8fcdf7c291aeb8786cb693c3a803ba2a59d6bac4c1facd239d778cc6f643d0a78920d9d278cba5d0cd46779b2546822"; }; | 128 | "jasmine-core@npm:4.0.1" = { filename = "jasmine-core-npm-4.0.1-e72087e57c-f097bf2c68.zip"; sha512 = "f097bf2c681acb55e03fb56a1569a9dfa8fcdf7c291aeb8786cb693c3a803ba2a59d6bac4c1facd239d778cc6f643d0a78920d9d278cba5d0cd46779b2546822"; }; |
| 127 | "karma-jasmine-html-reporter@npm:1.7.0" = { filename = "karma-jasmine-html-reporter-npm-1.7.0-87aa5c7cce-926c25858e.zip"; sha512 = "926c25858ea45115496524749881837de0091e3f8119e0f81a18c377e69fb82f832836b94e08d2a24fbc49aa21b491ae0bb861f07404f144c92bc84f409ef202"; }; | 129 | "karma-jasmine-html-reporter@npm:1.7.0" = { filename = "karma-jasmine-html-reporter-npm-1.7.0-87aa5c7cce-926c25858e.zip"; sha512 = "926c25858ea45115496524749881837de0091e3f8119e0f81a18c377e69fb82f832836b94e08d2a24fbc49aa21b491ae0bb861f07404f144c92bc84f409ef202"; }; |
| 128 | "tslib@npm:2.4.0" = { filename = "tslib-npm-2.4.0-9cb6dc5030-8c4aa6a3c5.zip"; sha512 = "8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113"; }; | 130 | "tslib@npm:2.4.0" = { filename = "tslib-npm-2.4.0-9cb6dc5030-8c4aa6a3c5.zip"; sha512 = "8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113"; }; |
| 131 | "uuid@npm:8.3.2" = { filename = "uuid-npm-8.3.2-eca0baba53-5575a8a75c.zip"; sha512 = "5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df"; }; | ||
| 129 | "typescript@npm:4.6.4" = { filename = "typescript-npm-4.6.4-114dfa5f7e-e7bfcc39cd.zip"; sha512 = "e7bfcc39cd4571a63a54e5ea21f16b8445268b9900bf55aee0e02ad981be576acc140eba24f1af5e3c1457767c96cea6d12861768fb386cf3ffb34013718631a"; }; | 132 | "typescript@npm:4.6.4" = { filename = "typescript-npm-4.6.4-114dfa5f7e-e7bfcc39cd.zip"; sha512 = "e7bfcc39cd4571a63a54e5ea21f16b8445268b9900bf55aee0e02ad981be576acc140eba24f1af5e3c1457767c96cea6d12861768fb386cf3ffb34013718631a"; }; |
| 130 | "@angular/animations@npm:13.3.9" = { filename = "@angular-animations-npm-13.3.9-e1d514c5f5-4c77f5678a.zip"; sha512 = "4c77f5678acc88cc6cba6e76c498bc0951dbcba2f985eded4972f830857def10918551a805882017655df8dbc8c39821ee8a5ca295d0abf7d82746799aa7f212"; }; | 133 | "@angular/animations@npm:13.3.9" = { filename = "@angular-animations-npm-13.3.9-e1d514c5f5-4c77f5678a.zip"; sha512 = "4c77f5678acc88cc6cba6e76c498bc0951dbcba2f985eded4972f830857def10918551a805882017655df8dbc8c39821ee8a5ca295d0abf7d82746799aa7f212"; }; |
| 131 | "@angular/common@npm:13.3.9" = { filename = "@angular-common-npm-13.3.9-9a7d731ff5-984af09c8a.zip"; sha512 = "984af09c8af87d78a85f743e6923c047c032489a97dd93b8d10702e7556cb842b63a2c836c339f418fe73cc0cd0ce50e262a27a5c721cd8bec35279376625219"; }; | 134 | "@angular/common@npm:13.3.9" = { filename = "@angular-common-npm-13.3.9-9a7d731ff5-984af09c8a.zip"; sha512 = "984af09c8af87d78a85f743e6923c047c032489a97dd93b8d10702e7556cb842b63a2c836c339f418fe73cc0cd0ce50e262a27a5c721cd8bec35279376625219"; }; |
| 132 | "@angular/compiler@npm:13.3.9" = { filename = "@angular-compiler-npm-13.3.9-06a00a359f-4e80398d61.zip"; sha512 = "4e80398d6121b70961ec3a7565ba897dcd63788b78c4011817fd7d3114a398f29c79f5ca985bffcc1e7e11a57dc89054e5cfef410720aacb9b9368647f452798"; }; | 135 | "@angular/compiler@npm:13.3.9" = { filename = "@angular-compiler-npm-13.3.9-06a00a359f-4e80398d61.zip"; sha512 = "4e80398d6121b70961ec3a7565ba897dcd63788b78c4011817fd7d3114a398f29c79f5ca985bffcc1e7e11a57dc89054e5cfef410720aacb9b9368647f452798"; }; |
| 133 | "@angular/core@npm:13.3.9" = { filename = "@angular-core-npm-13.3.9-8bfa1070cd-4b4ce78de4.zip"; sha512 = "4b4ce78de4aadfb6f5e2d8417157d60337052c02608137e5a6548d8e6a8ef9bb4edd29187a2b8ea2720527c20731797b5d0d97ab800e580ccc5e5b609caf80c5"; }; | 136 | "@angular/core@npm:13.3.9" = { filename = "@angular-core-npm-13.3.9-8bfa1070cd-4b4ce78de4.zip"; sha512 = "4b4ce78de4aadfb6f5e2d8417157d60337052c02608137e5a6548d8e6a8ef9bb4edd29187a2b8ea2720527c20731797b5d0d97ab800e580ccc5e5b609caf80c5"; }; |
| 137 | "@angular/flex-layout@npm:13.0.0-beta.38" = { filename = "@angular-flex-layout-npm-13.0.0-beta.38-a6255273fa-c5f22d6748.zip"; sha512 = "c5f22d6748ad8dc2ebc9fa57c82d80cb9f9942d66dfb0246315d351872a2c9df5fa95d849fb0d2750ca99812d5e27d073b7842e07e6f21e541cbe337d9f4e7ed"; }; | ||
| 134 | "@angular/forms@npm:13.3.9" = { filename = "@angular-forms-npm-13.3.9-bd7c125354-7d95a7c482.zip"; sha512 = "7d95a7c4828f84be28882320ce4ea685b7990bb0dd920417ce3285d4ad61231b546c27e75b8fc90c4a24629c1cb5ae7692af59f2d9992887672ee3461027683d"; }; | 138 | "@angular/forms@npm:13.3.9" = { filename = "@angular-forms-npm-13.3.9-bd7c125354-7d95a7c482.zip"; sha512 = "7d95a7c4828f84be28882320ce4ea685b7990bb0dd920417ce3285d4ad61231b546c27e75b8fc90c4a24629c1cb5ae7692af59f2d9992887672ee3461027683d"; }; |
| 135 | "@angular/material@npm:13.3.7" = { filename = "@angular-material-npm-13.3.7-5b8dc87806-a98ea8117e.zip"; sha512 = "a98ea8117e381412d77f972247f56b87542c01e97964ef6b6d369df5c0a9bc3d247886ad22b235a4e5a471f20e1724297036c9acfcdd2d03edaa4bedd8f7bbf8"; }; | 139 | "@angular/material@npm:13.3.7" = { filename = "@angular-material-npm-13.3.7-5b8dc87806-a98ea8117e.zip"; sha512 = "a98ea8117e381412d77f972247f56b87542c01e97964ef6b6d369df5c0a9bc3d247886ad22b235a4e5a471f20e1724297036c9acfcdd2d03edaa4bedd8f7bbf8"; }; |
| 136 | "@angular/platform-browser-dynamic@npm:13.3.9" = { filename = "@angular-platform-browser-dynamic-npm-13.3.9-3414000d2b-34967ed4fd.zip"; sha512 = "34967ed4fd6b81bacdef17432334e6c99656936d0f4625d8fae854f0984868e6ce0b5c497fb028c68971d09eaf85080ce201ee023c803c1126970a2a831528cf"; }; | 140 | "@angular/platform-browser-dynamic@npm:13.3.9" = { filename = "@angular-platform-browser-dynamic-npm-13.3.9-3414000d2b-34967ed4fd.zip"; sha512 = "34967ed4fd6b81bacdef17432334e6c99656936d0f4625d8fae854f0984868e6ce0b5c497fb028c68971d09eaf85080ce201ee023c803c1126970a2a831528cf"; }; |
| @@ -138,9 +142,11 @@ cacheEntries = { | |||
| 138 | "@angular/router@npm:13.3.9" = { filename = "@angular-router-npm-13.3.9-c3b3c739a8-e675ff9a3e.zip"; sha512 = "e675ff9a3e33c92041c897601d633372a3cfaf502d4779b01469f1f26798d0bdf9b2b411082fe64d997bda0c75f7f12fbb32013286447b1398bc70565df028cb"; }; | 142 | "@angular/router@npm:13.3.9" = { filename = "@angular-router-npm-13.3.9-c3b3c739a8-e675ff9a3e.zip"; sha512 = "e675ff9a3e33c92041c897601d633372a3cfaf502d4779b01469f1f26798d0bdf9b2b411082fe64d997bda0c75f7f12fbb32013286447b1398bc70565df028cb"; }; |
| 139 | "karma-chrome-launcher@npm:3.1.1" = { filename = "karma-chrome-launcher-npm-3.1.1-871ed2dfa7-8442219105.zip"; sha512 = "8442219105e1f11a9284fd47f2e21e34720f7e725f25ea08f7525a7ec2088e2c1b65e2def4d7780139d296afc5c30bf4e1d4a839a097eb814031c2f6b379b39f"; }; | 143 | "karma-chrome-launcher@npm:3.1.1" = { filename = "karma-chrome-launcher-npm-3.1.1-871ed2dfa7-8442219105.zip"; sha512 = "8442219105e1f11a9284fd47f2e21e34720f7e725f25ea08f7525a7ec2088e2c1b65e2def4d7780139d296afc5c30bf4e1d4a839a097eb814031c2f6b379b39f"; }; |
| 140 | "karma-jasmine@npm:4.0.2" = { filename = "karma-jasmine-npm-4.0.2-8647c711ec-bf884704af.zip"; sha512 = "bf884704af1fd19816d9f4e96b25e286ff1a57adcabe1f15e3d2b3e9c1da873c1c843b9eab4274c27e63a99f1c3dea864f1f5eca1a10dc065e6e9d5796c207b4"; }; | 144 | "karma-jasmine@npm:4.0.2" = { filename = "karma-jasmine-npm-4.0.2-8647c711ec-bf884704af.zip"; sha512 = "bf884704af1fd19816d9f4e96b25e286ff1a57adcabe1f15e3d2b3e9c1da873c1c843b9eab4274c27e63a99f1c3dea864f1f5eca1a10dc065e6e9d5796c207b4"; }; |
| 145 | "ngx-webstorage@npm:9.0.0" = { filename = "ngx-webstorage-npm-9.0.0-64bf01f819-988ecf069d.zip"; sha512 = "988ecf069d3bdf2c8108cc3e011410eb2e951b6b7932ceddcd5bb1141d42cabd62be97e2d248dd495d5ce4917934ef866a431a0afecd8d90de1040978a933b82"; }; | ||
| 141 | "rxjs@npm:7.5.5" = { filename = "rxjs-npm-7.5.5-d0546b1ccb-e034f60805.zip"; sha512 = "e034f60805210cce756dd2f49664a8108780b117cf5d0e2281506e9e6387f7b4f1532d974a8c8b09314fa7a16dd2f6cff3462072a5789672b5dcb45c4173f3c6"; }; | 146 | "rxjs@npm:7.5.5" = { filename = "rxjs-npm-7.5.5-d0546b1ccb-e034f60805.zip"; sha512 = "e034f60805210cce756dd2f49664a8108780b117cf5d0e2281506e9e6387f7b4f1532d974a8c8b09314fa7a16dd2f6cff3462072a5789672b5dcb45c4173f3c6"; }; |
| 142 | "zone.js@npm:0.11.5" = { filename = "zone.js-npm-0.11.5-78b46cd237-7dba3af83c.zip"; sha512 = "7dba3af83cc68e881e8a5cc549e4a7bab5f32c5289838f14eb251871816210a39ed8bcd89030593b56d469b4aa8271d644e71ad408fcadd4ccd61b8c7203f2ef"; }; | 147 | "zone.js@npm:0.11.5" = { filename = "zone.js-npm-0.11.5-78b46cd237-7dba3af83c.zip"; sha512 = "7dba3af83cc68e881e8a5cc549e4a7bab5f32c5289838f14eb251871816210a39ed8bcd89030593b56d469b4aa8271d644e71ad408fcadd4ccd61b8c7203f2ef"; }; |
| 143 | "@angular/cdk@npm:13.3.7" = { filename = "@angular-cdk-npm-13.3.7-58a23cc0a3-a2944da577.zip"; sha512 = "a2944da5774a67956fe48762748b2e24dbd58be760652aca5c22fcfa67ad3e01635f6890c689596fe1525d6dbb2a7bd46ed616bdc4d9ee05db64d849b410cbf9"; }; | 148 | "@angular/cdk@npm:13.3.7" = { filename = "@angular-cdk-npm-13.3.7-58a23cc0a3-a2944da577.zip"; sha512 = "a2944da5774a67956fe48762748b2e24dbd58be760652aca5c22fcfa67ad3e01635f6890c689596fe1525d6dbb2a7bd46ed616bdc4d9ee05db64d849b410cbf9"; }; |
| 149 | "jwt-decode@npm:3.1.2" = { filename = "jwt-decode-npm-3.1.2-bf3ab26591-20a4b072d4.zip"; sha512 = "20a4b072d44ce3479f42d0d2c8d3dabeb353081ba4982e40b83a779f2459a70be26441be6c160bfc8c3c6eadf9f6380a036fbb06ac5406b5674e35d8c4205eeb"; }; | ||
| 144 | "jasmine-core@npm:3.99.1" = { filename = "jasmine-core-npm-3.99.1-07f52d92cc-4e4a89739d.zip"; sha512 = "4e4a89739d99e471b86c7ccc4c5c244a77cc6d1e17b2b0d87d81266b8415697354d8873f7e764790a10661744f73a753a6e9bcd9b3e48c66a0c9b8a092b071b7"; }; | 150 | "jasmine-core@npm:3.99.1" = { filename = "jasmine-core-npm-3.99.1-07f52d92cc-4e4a89739d.zip"; sha512 = "4e4a89739d99e471b86c7ccc4c5c244a77cc6d1e17b2b0d87d81266b8415697354d8873f7e764790a10661744f73a753a6e9bcd9b3e48c66a0c9b8a092b071b7"; }; |
| 145 | "parse5@npm:5.1.1" = { filename = "parse5-npm-5.1.1-8e63d82cff-613a714af4.zip"; sha512 = "613a714af4c1101d1cb9f7cece2558e35b9ae8a0c03518223a4a1e35494624d9a9ad5fad4c13eab66a0e0adccd9aa3d522fc8f5f9cc19789e0579f3fa0bdfc65"; }; | 151 | "parse5@npm:5.1.1" = { filename = "parse5-npm-5.1.1-8e63d82cff-613a714af4.zip"; sha512 = "613a714af4c1101d1cb9f7cece2558e35b9ae8a0c03518223a4a1e35494624d9a9ad5fad4c13eab66a0e0adccd9aa3d522fc8f5f9cc19789e0579f3fa0bdfc65"; }; |
| 146 | "which@npm:1.3.1" = { filename = "which-npm-1.3.1-f0ebb8bdd8-f2e185c624.zip"; sha512 = "f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04"; }; | 152 | "which@npm:1.3.1" = { filename = "which-npm-1.3.1-f0ebb8bdd8-f2e185c624.zip"; sha512 = "f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04"; }; |
| @@ -182,7 +188,6 @@ cacheEntries = { | |||
| 182 | "ini@npm:2.0.0" = { filename = "ini-npm-2.0.0-28f7426761-e7aadc5fb2.zip"; sha512 = "e7aadc5fb2e4aefc666d74ee2160c073995a4061556b1b5b4241ecb19ad609243b9cceafe91bae49c219519394bbd31512516cb22a3b1ca6e66d869e0447e84e"; }; | 188 | "ini@npm:2.0.0" = { filename = "ini-npm-2.0.0-28f7426761-e7aadc5fb2.zip"; sha512 = "e7aadc5fb2e4aefc666d74ee2160c073995a4061556b1b5b4241ecb19ad609243b9cceafe91bae49c219519394bbd31512516cb22a3b1ca6e66d869e0447e84e"; }; |
| 183 | "jsonc-parser@npm:3.0.0" = { filename = "jsonc-parser-npm-3.0.0-66e692e88a-1df2326f1f.zip"; sha512 = "1df2326f1f9688de30c70ff19c5b2a83ba3b89a1036160da79821d1361090775e9db502dc57a67c11b56e1186fc1ed70b887f25c5febf9a3ec4f91435836c99d"; }; | 189 | "jsonc-parser@npm:3.0.0" = { filename = "jsonc-parser-npm-3.0.0-66e692e88a-1df2326f1f.zip"; sha512 = "1df2326f1f9688de30c70ff19c5b2a83ba3b89a1036160da79821d1361090775e9db502dc57a67c11b56e1186fc1ed70b887f25c5febf9a3ec4f91435836c99d"; }; |
| 184 | "symbol-observable@npm:4.0.0" = { filename = "symbol-observable-npm-4.0.0-5c36594410-212c7edce6.zip"; sha512 = "212c7edce6186634d671336a88c0e0bbd626c2ab51ed57498dc90698cce541839a261b969c2a1e8dd43762133d47672e8b62e0b1ce9cf4157934ba45fd172ba8"; }; | 190 | "symbol-observable@npm:4.0.0" = { filename = "symbol-observable-npm-4.0.0-5c36594410-212c7edce6.zip"; sha512 = "212c7edce6186634d671336a88c0e0bbd626c2ab51ed57498dc90698cce541839a261b969c2a1e8dd43762133d47672e8b62e0b1ce9cf4157934ba45fd172ba8"; }; |
| 185 | "uuid@npm:8.3.2" = { filename = "uuid-npm-8.3.2-eca0baba53-5575a8a75c.zip"; sha512 = "5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df"; }; | ||
| 186 | "debug@npm:4.3.3" = { filename = "debug-npm-4.3.3-710fd4cc7f-14472d56fe.zip"; sha512 = "14472d56fe4a94dbcfaa6dbed2dd3849f1d72ba78104a1a328047bb564643ca49df0224c3a17fa63533fd11dd3d4c8636cd861191232a2c6735af00cc2d4de16"; }; | 191 | "debug@npm:4.3.3" = { filename = "debug-npm-4.3.3-710fd4cc7f-14472d56fe.zip"; sha512 = "14472d56fe4a94dbcfaa6dbed2dd3849f1d72ba78104a1a328047bb564643ca49df0224c3a17fa63533fd11dd3d4c8636cd861191232a2c6735af00cc2d4de16"; }; |
| 187 | "semver@npm:7.3.5" = { filename = "semver-npm-7.3.5-618cf5db6a-5eafe6102b.zip"; sha512 = "5eafe6102bea2a7439897c1856362e31cc348ccf96efd455c8b5bc2c61e6f7e7b8250dc26b8828c1d76a56f818a7ee907a36ae9fb37a599d3d24609207001d60"; }; | 192 | "semver@npm:7.3.5" = { filename = "semver-npm-7.3.5-618cf5db6a-5eafe6102b.zip"; sha512 = "5eafe6102bea2a7439897c1856362e31cc348ccf96efd455c8b5bc2c61e6f7e7b8250dc26b8828c1d76a56f818a7ee907a36ae9fb37a599d3d24609207001d60"; }; |
| 188 | "yallist@npm:4.0.0" = { filename = "yallist-npm-4.0.0-b493d9e907-343617202a.zip"; sha512 = "343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5"; }; | 193 | "yallist@npm:4.0.0" = { filename = "yallist-npm-4.0.0-b493d9e907-343617202a.zip"; sha512 = "343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5"; }; |
| @@ -984,7 +989,6 @@ cacheEntries = { | |||
| 984 | "jsesc@npm:0.5.0" = { filename = "jsesc-npm-0.5.0-6827074492-b8b44cbfc9.zip"; sha512 = "b8b44cbfc92f198ad972fba706ee6a1dfa7485321ee8c0b25f5cedd538dcb20cde3197de16a7265430fce8277a12db066219369e3d51055038946039f6e20e17"; }; | 989 | "jsesc@npm:0.5.0" = { filename = "jsesc-npm-0.5.0-6827074492-b8b44cbfc9.zip"; sha512 = "b8b44cbfc92f198ad972fba706ee6a1dfa7485321ee8c0b25f5cedd538dcb20cde3197de16a7265430fce8277a12db066219369e3d51055038946039f6e20e17"; }; |
| 985 | "unicode-canonical-property-names-ecmascript@npm:2.0.0" = { filename = "unicode-canonical-property-names-ecmascript-npm-2.0.0-d2d8554a14-39be078afd.zip"; sha512 = "39be078afd014c14dcd957a7a46a60061bc37c4508ba146517f85f60361acf4c7539552645ece25de840e17e293baa5556268d091ca6762747fdd0c705001a45"; }; | 990 | "unicode-canonical-property-names-ecmascript@npm:2.0.0" = { filename = "unicode-canonical-property-names-ecmascript-npm-2.0.0-d2d8554a14-39be078afd.zip"; sha512 = "39be078afd014c14dcd957a7a46a60061bc37c4508ba146517f85f60361acf4c7539552645ece25de840e17e293baa5556268d091ca6762747fdd0c705001a45"; }; |
| 986 | "unicode-property-aliases-ecmascript@npm:2.0.0" = { filename = "unicode-property-aliases-ecmascript-npm-2.0.0-1636cb7768-dda4d39128.zip"; sha512 = "dda4d39128cbbede2ac60fbb85493d979ec65913b8a486bf7cb7a375a2346fa48cbf9dc6f1ae23376e7e8e684c2b411434891e151e865a661b40a85407db51d0"; }; | 991 | "unicode-property-aliases-ecmascript@npm:2.0.0" = { filename = "unicode-property-aliases-ecmascript-npm-2.0.0-1636cb7768-dda4d39128.zip"; sha512 = "dda4d39128cbbede2ac60fbb85493d979ec65913b8a486bf7cb7a375a2346fa48cbf9dc6f1ae23376e7e8e684c2b411434891e151e865a661b40a85407db51d0"; }; |
| 987 | "ngx-webstorage@npm:9.0.0" = { filename = "ngx-webstorage-npm-9.0.0-64bf01f819-988ecf069d.zip"; sha512 = "988ecf069d3bdf2c8108cc3e011410eb2e951b6b7932ceddcd5bb1141d42cabd62be97e2d248dd495d5ce4917934ef866a431a0afecd8d90de1040978a933b82"; }; | ||
| 988 | }; | 992 | }; |
| 989 | 993 | ||
| 990 | in optionalOverride overrideAttrs project | 994 | in optionalOverride overrideAttrs project |
diff --git a/overlays/spm/frontend/yarn.lock b/overlays/spm/frontend/yarn.lock index 21ace08d..9bd93e91 100644 --- a/overlays/spm/frontend/yarn.lock +++ b/overlays/spm/frontend/yarn.lock | |||
| @@ -283,6 +283,21 @@ __metadata: | |||
| 283 | languageName: node | 283 | languageName: node |
| 284 | linkType: hard | 284 | linkType: hard |
| 285 | 285 | ||
| 286 | "@angular/flex-layout@npm:^13.0.0-beta.38": | ||
| 287 | version: 13.0.0-beta.38 | ||
| 288 | resolution: "@angular/flex-layout@npm:13.0.0-beta.38" | ||
| 289 | dependencies: | ||
| 290 | tslib: ^2.3.0 | ||
| 291 | peerDependencies: | ||
| 292 | "@angular/cdk": ^13.0.0 | ||
| 293 | "@angular/common": ^13.0.0 | ||
| 294 | "@angular/core": ^13.0.0 | ||
| 295 | "@angular/platform-browser": ^13.0.0 | ||
| 296 | rxjs: ^6.5.3 || ^7.4.0 | ||
| 297 | checksum: c5f22d6748ad8dc2ebc9fa57c82d80cb9f9942d66dfb0246315d351872a2c9df5fa95d849fb0d2750ca99812d5e27d073b7842e07e6f21e541cbe337d9f4e7ed | ||
| 298 | languageName: node | ||
| 299 | linkType: hard | ||
| 300 | |||
| 286 | "@angular/forms@npm:~13.3.0": | 301 | "@angular/forms@npm:~13.3.0": |
| 287 | version: 13.3.9 | 302 | version: 13.3.9 |
| 288 | resolution: "@angular/forms@npm:13.3.9" | 303 | resolution: "@angular/forms@npm:13.3.9" |
| @@ -1710,6 +1725,13 @@ __metadata: | |||
| 1710 | languageName: node | 1725 | languageName: node |
| 1711 | linkType: hard | 1726 | linkType: hard |
| 1712 | 1727 | ||
| 1728 | "@fontsource/source-sans-pro@npm:^4.5.10": | ||
| 1729 | version: 4.5.10 | ||
| 1730 | resolution: "@fontsource/source-sans-pro@npm:4.5.10" | ||
| 1731 | checksum: d0b786cdff56e2b9f0dbb557473342ac95bc2021eea57097a29244936554a1f452b1ba5b053f11f2f3bcf58ba77823ac1e2a3ff8aba8c4618026a131a025a86d | ||
| 1732 | languageName: node | ||
| 1733 | linkType: hard | ||
| 1734 | |||
| 1713 | "@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": | 1735 | "@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": |
| 1714 | version: 1.1.3 | 1736 | version: 1.1.3 |
| 1715 | resolution: "@gar/promisify@npm:1.1.3" | 1737 | resolution: "@gar/promisify@npm:1.1.3" |
| @@ -2164,6 +2186,13 @@ __metadata: | |||
| 2164 | languageName: node | 2186 | languageName: node |
| 2165 | linkType: hard | 2187 | linkType: hard |
| 2166 | 2188 | ||
| 2189 | "@types/uuid@npm:^8.3.4": | ||
| 2190 | version: 8.3.4 | ||
| 2191 | resolution: "@types/uuid@npm:8.3.4" | ||
| 2192 | checksum: 6f11f3ff70f30210edaa8071422d405e9c1d4e53abbe50fdce365150d3c698fe7bbff65c1e71ae080cbfb8fded860dbb5e174da96fdbbdfcaa3fb3daa474d20f | ||
| 2193 | languageName: node | ||
| 2194 | linkType: hard | ||
| 2195 | |||
| 2167 | "@types/ws@npm:^8.2.2": | 2196 | "@types/ws@npm:^8.2.2": |
| 2168 | version: 8.5.3 | 2197 | version: 8.5.3 |
| 2169 | resolution: "@types/ws@npm:8.5.3" | 2198 | resolution: "@types/ws@npm:8.5.3" |
| @@ -5457,6 +5486,13 @@ __metadata: | |||
| 5457 | languageName: node | 5486 | languageName: node |
| 5458 | linkType: hard | 5487 | linkType: hard |
| 5459 | 5488 | ||
| 5489 | "jwt-decode@npm:^3.1.2": | ||
| 5490 | version: 3.1.2 | ||
| 5491 | resolution: "jwt-decode@npm:3.1.2" | ||
| 5492 | checksum: 20a4b072d44ce3479f42d0d2c8d3dabeb353081ba4982e40b83a779f2459a70be26441be6c160bfc8c3c6eadf9f6380a036fbb06ac5406b5674e35d8c4205eeb | ||
| 5493 | languageName: node | ||
| 5494 | linkType: hard | ||
| 5495 | |||
| 5460 | "karma-chrome-launcher@npm:~3.1.0": | 5496 | "karma-chrome-launcher@npm:~3.1.0": |
| 5461 | version: 3.1.1 | 5497 | version: 3.1.1 |
| 5462 | resolution: "karma-chrome-launcher@npm:3.1.1" | 5498 | resolution: "karma-chrome-launcher@npm:3.1.1" |
| @@ -8083,16 +8119,20 @@ __metadata: | |||
| 8083 | "@angular/compiler": ~13.3.0 | 8119 | "@angular/compiler": ~13.3.0 |
| 8084 | "@angular/compiler-cli": ~13.3.0 | 8120 | "@angular/compiler-cli": ~13.3.0 |
| 8085 | "@angular/core": ~13.3.0 | 8121 | "@angular/core": ~13.3.0 |
| 8122 | "@angular/flex-layout": ^13.0.0-beta.38 | ||
| 8086 | "@angular/forms": ~13.3.0 | 8123 | "@angular/forms": ~13.3.0 |
| 8087 | "@angular/material": 13.3.7 | 8124 | "@angular/material": 13.3.7 |
| 8088 | "@angular/platform-browser": ~13.3.0 | 8125 | "@angular/platform-browser": ~13.3.0 |
| 8089 | "@angular/platform-browser-dynamic": ~13.3.0 | 8126 | "@angular/platform-browser-dynamic": ~13.3.0 |
| 8090 | "@angular/router": ~13.3.0 | 8127 | "@angular/router": ~13.3.0 |
| 8091 | "@fontsource/roboto": ^4.5.7 | 8128 | "@fontsource/roboto": ^4.5.7 |
| 8129 | "@fontsource/source-sans-pro": ^4.5.10 | ||
| 8092 | "@material-icons/svg": ^1.0.28 | 8130 | "@material-icons/svg": ^1.0.28 |
| 8093 | "@types/jasmine": ~3.10.0 | 8131 | "@types/jasmine": ~3.10.0 |
| 8094 | "@types/node": ^12.11.1 | 8132 | "@types/node": ^12.11.1 |
| 8133 | "@types/uuid": ^8.3.4 | ||
| 8095 | jasmine-core: ~4.0.0 | 8134 | jasmine-core: ~4.0.0 |
| 8135 | jwt-decode: ^3.1.2 | ||
| 8096 | karma: ~6.3.0 | 8136 | karma: ~6.3.0 |
| 8097 | karma-chrome-launcher: ~3.1.0 | 8137 | karma-chrome-launcher: ~3.1.0 |
| 8098 | karma-coverage: ~2.1.0 | 8138 | karma-coverage: ~2.1.0 |
| @@ -8102,6 +8142,7 @@ __metadata: | |||
| 8102 | rxjs: ~7.5.0 | 8142 | rxjs: ~7.5.0 |
| 8103 | tslib: ^2.3.0 | 8143 | tslib: ^2.3.0 |
| 8104 | typescript: ~4.6.2 | 8144 | typescript: ~4.6.2 |
| 8145 | uuid: ^8.3.2 | ||
| 8105 | zone.js: ~0.11.4 | 8146 | zone.js: ~0.11.4 |
| 8106 | languageName: unknown | 8147 | languageName: unknown |
| 8107 | linkType: soft | 8148 | linkType: soft |
diff --git a/overlays/spm/lib/Spm/Api.hs b/overlays/spm/lib/Spm/Api.hs index 14acfac4..c44a7951 100644 --- a/overlays/spm/lib/Spm/Api.hs +++ b/overlays/spm/lib/Spm/Api.hs | |||
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | module Spm.Api | 3 | module Spm.Api |
| 4 | ( SpmStyle(..), _SpmWords, _SpmConsonants | 4 | ( SpmStyle(..), _SpmWords, _SpmConsonants |
| 5 | , SpmMailbox | 5 | , SpmMailbox, SpmDomain |
| 6 | , SpmApi, spmApi | 6 | , SpmApi, spmApi |
| 7 | ) where | 7 | ) where |
| 8 | 8 | ||
| @@ -30,6 +30,8 @@ import Crypto.JWT.Instances () | |||
| 30 | import Data.UUID (UUID) | 30 | import Data.UUID (UUID) |
| 31 | import Data.UUID.Instances () | 31 | import Data.UUID.Instances () |
| 32 | 32 | ||
| 33 | import qualified Data.Aeson as JSON | ||
| 34 | |||
| 33 | -- import Data.Aeson (ToJSON, FromJSON) | 35 | -- import Data.Aeson (ToJSON, FromJSON) |
| 34 | 36 | ||
| 35 | 37 | ||
| @@ -50,8 +52,20 @@ instance FromHttpApiData SpmStyle where | |||
| 50 | 52 | ||
| 51 | newtype SpmMailbox = SpmMailbox { unSpmMailbox :: CI Text } | 53 | newtype SpmMailbox = SpmMailbox { unSpmMailbox :: CI Text } |
| 52 | deriving stock (Eq, Ord, Read, Show, Generic, Typeable) | 54 | deriving stock (Eq, Ord, Read, Show, Generic, Typeable) |
| 53 | deriving newtype (MimeRender JSON, MimeRender PlainText) | 55 | deriving newtype (MimeRender PlainText) |
| 54 | makeWrapped ''SpmMailbox | 56 | makeWrapped ''SpmMailbox |
| 57 | |||
| 58 | instance MimeRender JSON SpmMailbox where | ||
| 59 | mimeRender p mbox = mimeRender p $ JSON.object [ "mailbox" JSON..= unSpmMailbox mbox ] | ||
| 60 | |||
| 61 | newtype SpmDomain = SpmDomain { unSpmDomain :: CI Text } | ||
| 62 | deriving stock (Eq, Ord, Read, Show, Generic, Typeable) | ||
| 63 | deriving newtype (MimeRender PlainText) | ||
| 64 | makeWrapped ''SpmDomain | ||
| 65 | |||
| 66 | instance MimeRender JSON SpmDomain where | ||
| 67 | mimeRender p dom = mimeRender p $ JSON.object [ "domain" JSON..= unSpmDomain dom ] | ||
| 68 | |||
| 55 | -- newtype SpmLocal = SpmLocal | 69 | -- newtype SpmLocal = SpmLocal |
| 56 | -- { unSpmLocal :: CI Text | 70 | -- { unSpmLocal :: CI Text |
| 57 | -- } deriving stock (Eq, Ord, Read, Show, Generic, Typeable) | 71 | -- } deriving stock (Eq, Ord, Read, Show, Generic, Typeable) |
| @@ -79,6 +93,7 @@ makeWrapped ''SpmMailbox | |||
| 79 | -- ] | 93 | -- ] |
| 80 | 94 | ||
| 81 | type SpmApi = "whoami" :> Get '[PlainText, JSON] SpmMailbox | 95 | type SpmApi = "whoami" :> Get '[PlainText, JSON] SpmMailbox |
| 96 | :<|> "domain" :> Get '[PlainText, JSON] SpmDomain | ||
| 82 | :<|> "jwks.json" :> Get '[JSON] JWKSet | 97 | :<|> "jwks.json" :> Get '[JSON] JWKSet |
| 83 | :<|> "instance-id" :> Get '[PlainText, JSON, OctetStream] UUID | 98 | :<|> "instance-id" :> Get '[PlainText, JSON, OctetStream] UUID |
| 84 | :<|> "spm" :> "generate" :> QueryParam "style" SpmStyle :> Get '[PlainText, JSON, OctetStream] SignedJWT | 99 | :<|> "spm" :> "generate" :> QueryParam "style" SpmStyle :> Get '[PlainText, JSON, OctetStream] SignedJWT |
diff --git a/overlays/spm/server/Spm/Server.hs b/overlays/spm/server/Spm/Server.hs index 1f785999..0dd3e810 100644 --- a/overlays/spm/server/Spm/Server.hs +++ b/overlays/spm/server/Spm/Server.hs | |||
| @@ -114,6 +114,7 @@ type SpmServerApi = Header' '[Required, Strict] "SPM-Domain" MailDomain | |||
| 114 | :> AuthProtect "spm_mailbox" | 114 | :> AuthProtect "spm_mailbox" |
| 115 | :> SpmApi | 115 | :> SpmApi |
| 116 | :<|> "ui" :> Raw | 116 | :<|> "ui" :> Raw |
| 117 | :<|> GetNoContent | ||
| 117 | 118 | ||
| 118 | spmServerApi :: Proxy SpmServerApi | 119 | spmServerApi :: Proxy SpmServerApi |
| 119 | spmServerApi = Proxy | 120 | spmServerApi = Proxy |
| @@ -183,11 +184,15 @@ mkSpmApp = do | |||
| 183 | 184 | ||
| 184 | spmServer' = spmServer | 185 | spmServer' = spmServer |
| 185 | :<|> Tagged uiServer | 186 | :<|> Tagged uiServer |
| 187 | :<|> uiRedirect | ||
| 186 | 188 | ||
| 187 | logger <- askLoggerIO | 189 | logger <- askLoggerIO |
| 188 | return $ serveWithContextT spmServerApi spmServerContext ((runReaderT ?? ServerCtx{..}) . hoist (runLoggingT ?? logger)) spmServer' | 190 | return $ serveWithContextT spmServerApi spmServerContext ((runReaderT ?? ServerCtx{..}) . hoist (runLoggingT ?? logger)) spmServer' |
| 189 | & requestLogger | 191 | & requestLogger |
| 190 | 192 | ||
| 193 | where | ||
| 194 | uiRedirect = throwError err302 { errHeaders = [("Location", "/ui")] } | ||
| 195 | |||
| 191 | spmSql :: ReaderT SqlBackend Handler' a -> Handler' a | 196 | spmSql :: ReaderT SqlBackend Handler' a -> Handler' a |
| 192 | spmSql act = do | 197 | spmSql act = do |
| 193 | sqlPool <- view sctxSqlPool | 198 | sqlPool <- view sctxSqlPool |
| @@ -208,15 +213,18 @@ generateLocal SpmConsonants = fmap (review _Wrapped . CI.mk) . liftIO $ do | |||
| 208 | 213 | ||
| 209 | spmServer :: MailDomain -> MailMailbox -> Server' SpmApi | 214 | spmServer :: MailDomain -> MailMailbox -> Server' SpmApi |
| 210 | spmServer dom mbox = whoami | 215 | spmServer dom mbox = whoami |
| 211 | :<|> jwkSet | 216 | :<|> domain |
| 212 | :<|> instanceId | 217 | :<|> jwkSet |
| 213 | :<|> generate | 218 | :<|> instanceId |
| 214 | :<|> claim | 219 | :<|> generate |
| 220 | :<|> claim | ||
| 215 | where | 221 | where |
| 216 | whoami = do | 222 | whoami = do |
| 217 | Entity _ Mailbox{mailboxIdent} <- maybe (throwError err404) return <=< spmSql . getBy $ UniqueMailbox mbox | 223 | Entity _ Mailbox{mailboxIdent} <- maybe (throwError err404) return <=< spmSql . getBy $ UniqueMailbox mbox |
| 218 | return $ mailboxIdent ^. _Wrapped . re _Wrapped | 224 | return $ mailboxIdent ^. _Wrapped . re _Wrapped |
| 219 | 225 | ||
| 226 | domain = return $ dom ^. _Wrapped . re _Wrapped | ||
| 227 | |||
| 220 | jwkSet = views sctxJwkSet $ over _Wrapped (^.. folded . asPublicKey . _Just) | 228 | jwkSet = views sctxJwkSet $ over _Wrapped (^.. folded . asPublicKey . _Just) |
| 221 | 229 | ||
| 222 | instanceId = view sctxInstanceId | 230 | instanceId = view sctxInstanceId |
diff --git a/overlays/worktime/default.nix b/overlays/worktime/default.nix index d400648b..67be75ae 100644 --- a/overlays/worktime/default.nix +++ b/overlays/worktime/default.nix | |||
| @@ -5,7 +5,9 @@ | |||
| 5 | 5 | ||
| 6 | phases = [ "buildPhase" "checkPhase" "installPhase" ]; | 6 | phases = [ "buildPhase" "checkPhase" "installPhase" ]; |
| 7 | 7 | ||
| 8 | python = prev.python39.withPackages (ps: with ps; [pyxdg dateutil uritools requests configparser tabulate]); | 8 | buildInputs = [ |
| 9 | (prev.python39.withPackages (ps: with ps; [pyxdg dateutil uritools requests configparser tabulate])) | ||
| 10 | ]; | ||
| 9 | 11 | ||
| 10 | buildPhase = '' | 12 | buildPhase = '' |
| 11 | substituteAll $src worktime | 13 | substituteAll $src worktime |
| @@ -13,7 +15,7 @@ | |||
| 13 | 15 | ||
| 14 | doCheck = true; | 16 | doCheck = true; |
| 15 | checkPhase = '' | 17 | checkPhase = '' |
| 16 | ${python}/bin/python -m py_compile worktime | 18 | python -m py_compile worktime |
| 17 | ''; | 19 | ''; |
| 18 | 20 | ||
| 19 | installPhase = '' | 21 | installPhase = '' |
