diff options
Diffstat (limited to 'overlays/spm')
19 files changed, 470 insertions, 105 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 |