summaryrefslogtreecommitdiff
path: root/overlays/spm
diff options
context:
space:
mode:
authorGregor Kleen <gkleen@yggdrasil.li>2022-06-07 21:38:00 +0200
committerGregor Kleen <gkleen@yggdrasil.li>2022-06-07 21:38:00 +0200
commit3bf77d7c3144b16d55f35998dddc0d67bb8c17b2 (patch)
tree805b343e50d733240b3dae8700d43060943b4dab /overlays/spm
parentfc6cf6169868e60c189e4b243330c3717ff159f3 (diff)
downloadnixos-3bf77d7c3144b16d55f35998dddc0d67bb8c17b2.tar
nixos-3bf77d7c3144b16d55f35998dddc0d67bb8c17b2.tar.gz
nixos-3bf77d7c3144b16d55f35998dddc0d67bb8c17b2.tar.bz2
nixos-3bf77d7c3144b16d55f35998dddc0d67bb8c17b2.tar.xz
nixos-3bf77d7c3144b16d55f35998dddc0d67bb8c17b2.zip
...
Diffstat (limited to 'overlays/spm')
-rw-r--r--overlays/spm/frontend/angular.json2
-rw-r--r--overlays/spm/frontend/package.json5
-rw-r--r--overlays/spm/frontend/src/app/app-routing.module.ts11
-rw-r--r--overlays/spm/frontend/src/app/app.component.html28
-rw-r--r--overlays/spm/frontend/src/app/app.component.sass2
-rw-r--r--overlays/spm/frontend/src/app/app.component.ts89
-rw-r--r--overlays/spm/frontend/src/app/app.module.ts14
-rw-r--r--overlays/spm/frontend/src/app/color-scheme.service.ts62
-rw-r--r--overlays/spm/frontend/src/app/page-not-found/page-not-found.module.ts16
-rw-r--r--overlays/spm/frontend/src/app/spm/spm.component.html34
-rw-r--r--overlays/spm/frontend/src/app/spm/spm.component.sass13
-rw-r--r--overlays/spm/frontend/src/app/spm/spm.component.ts181
-rw-r--r--overlays/spm/frontend/src/app/spm/spm.module.ts31
-rw-r--r--overlays/spm/frontend/src/custom-theme.scss2
-rw-r--r--overlays/spm/frontend/src/styles.sass1
-rw-r--r--overlays/spm/frontend/yarn-project.nix8
-rw-r--r--overlays/spm/frontend/yarn.lock41
-rw-r--r--overlays/spm/lib/Spm/Api.hs19
-rw-r--r--overlays/spm/server/Spm/Server.hs16
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 @@
1import { NgModule } from '@angular/core'; 1import { NgModule } from '@angular/core';
2import { RouterModule, Routes } from '@angular/router'; 2import { RouterModule, Routes, PreloadAllModules } from '@angular/router';
3
4import { SpmComponent } from './spm/spm.component';
5import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
6 3
7const routes: Routes = [ 4const 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})
17export class AppRoutingModule { } 14export 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 @@
1import { Component, OnInit, OnDestroy } from '@angular/core'; 1import { Component, OnInit, OnDestroy, Inject, Renderer2, RendererFactory2 } from '@angular/core';
2import { DOCUMENT } from '@angular/common';
3import { MediaMatcher } from '@angular/cdk/layout';
4import { LocalStorage } from 'ngx-webstorage';
5import { BehaviorSubject, Subscription, Subject } from 'rxjs';
6import { Router, RouterEvent, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
7import { HttpClient, HttpHeaders } from '@angular/common/http';
2 8
3import { ColorSchemeService } from './color-scheme.service'; 9import { map, filter } from 'rxjs/operators';
10
11type ColorScheme = 'dark' | 'light';
12
13export 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})
10export class AppComponent implements OnInit, OnDestroy { 22export 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 @@
1import { NgModule } from '@angular/core'; 1import { NgModule } from '@angular/core';
2import { BrowserModule } from '@angular/platform-browser'; 2import { BrowserModule } from '@angular/platform-browser';
3import { NgxWebstorageModule } from 'ngx-webstorage';
3 4
4import { AppRoutingModule } from './app-routing.module'; 5import { AppRoutingModule } from './app-routing.module';
5import { AppComponent } from './app.component'; 6import { AppComponent } from './app.component';
6import { SpmComponent } from './spm/spm.component';
7import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
8import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
9import { MatIconModule } from '@angular/material/icon';
10import { HttpClientModule } from "@angular/common/http"; 8import { HttpClientModule } from "@angular/common/http";
11import { MatToolbarModule } from '@angular/material/toolbar'; 9import { MatToolbarModule } from '@angular/material/toolbar';
12import { MatButtonModule } from '@angular/material/button'; 10import { MatButtonModule } from '@angular/material/button';
11import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
12import { 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 @@
1import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
2import { MediaMatcher } from '@angular/cdk/layout';
3import { LocalStorage } from 'ngx-webstorage';
4
5type ColorScheme = "dark" | "light";
6
7@Injectable({
8 providedIn: 'root'
9})
10export 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 @@
1import { NgModule } from '@angular/core';
2import { PageNotFoundComponent } from './page-not-found.component';
3import { 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})
16export 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 @@
1import { Component, OnInit } from '@angular/core'; 1import { Component, OnInit, OnDestroy } from '@angular/core';
2import { MatIconRegistry, MatIconModule } from '@angular/material/icon'; 2import { MatIconRegistry, MatIconModule } from '@angular/material/icon';
3import { DomSanitizer } from "@angular/platform-browser"; 3import { DomSanitizer } from "@angular/platform-browser";
4import { BehaviorSubject, interval, Subscription, catchError, throwError } from 'rxjs';
5import { HttpClient, HttpHeaders, HttpErrorResponse, HttpResponse } from '@angular/common/http';
6
7import { v4 as uuidv4 } from 'uuid';
8import jwtDecode, { JwtPayload } from "jwt-decode";
9
10import { map, withLatestFrom } from 'rxjs/operators';
11
12export interface LoadingSpmMail {
13 state: "loading";
14}
15
16export 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
26export interface ExpiredSpmMail {
27 state: "expired";
28 local: string;
29 domain: string;
30}
31
32export interface ClaimedSpmMail {
33 state: "claimed";
34 local: string;
35 domain: string;
36}
37
38export interface SpmJwtUnregisteredPayload {
39 local: string;
40}
41
42type SpmJwtPayload = JwtPayload & SpmJwtUnregisteredPayload;
43
44export type SpmMail = LoadingSpmMail | LoadedSpmMail | ClaimedSpmMail | ExpiredSpmMail;
45
46function 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
62function 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})
10export class SpmComponent implements OnInit { 80export 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 @@
1import { NgModule } from '@angular/core';
2import { CommonModule } from '@angular/common';
3import { SpmComponent } from './spm.component';
4import { RouterModule } from '@angular/router';
5
6import { MatIconModule } from '@angular/material/icon';
7import { MatButtonModule } from '@angular/material/button';
8import { MatCardModule } from '@angular/material/card';
9import { FlexLayoutModule } from "@angular/flex-layout";
10import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
11import { 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})
31export 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
50html, body { height: 100%; } 50html, body { height: 100%; }
51body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 51body { 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
121cacheEntries = { 121cacheEntries = {
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
990in optionalOverride overrideAttrs project 994in 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
3module Spm.Api 3module 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 ()
30import Data.UUID (UUID) 30import Data.UUID (UUID)
31import Data.UUID.Instances () 31import Data.UUID.Instances ()
32 32
33import 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
51newtype SpmMailbox = SpmMailbox { unSpmMailbox :: CI Text } 53newtype 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)
54makeWrapped ''SpmMailbox 56makeWrapped ''SpmMailbox
57
58instance MimeRender JSON SpmMailbox where
59 mimeRender p mbox = mimeRender p $ JSON.object [ "mailbox" JSON..= unSpmMailbox mbox ]
60
61newtype SpmDomain = SpmDomain { unSpmDomain :: CI Text }
62 deriving stock (Eq, Ord, Read, Show, Generic, Typeable)
63 deriving newtype (MimeRender PlainText)
64makeWrapped ''SpmDomain
65
66instance 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
81type SpmApi = "whoami" :> Get '[PlainText, JSON] SpmMailbox 95type 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
118spmServerApi :: Proxy SpmServerApi 119spmServerApi :: Proxy SpmServerApi
119spmServerApi = Proxy 120spmServerApi = 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
191spmSql :: ReaderT SqlBackend Handler' a -> Handler' a 196spmSql :: ReaderT SqlBackend Handler' a -> Handler' a
192spmSql act = do 197spmSql 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
209spmServer :: MailDomain -> MailMailbox -> Server' SpmApi 214spmServer :: MailDomain -> MailMailbox -> Server' SpmApi
210spmServer dom mbox = whoami 215spmServer 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