@nauticana/sail 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ A shared Angular component library for building CRUD-based admin frontends. Prov
|
|
|
4
4
|
|
|
5
5
|
> **Compatibility:** sail and keel are versioned in lock-step. The current line is **sail v1.0.x ↔ keel v1.0.x**. Earlier lines: **sail v0.5.x ↔ keel v0.5.x**, **sail v0.6.x / v0.7.x ↔ keel v0.7.x**, **sail v0.8.x ↔ keel v0.8.x**, **sail v0.9.x ↔ keel v0.9.x**. Newer sail releases extend the contract — older keel servers reject unknown endpoints with HTTP 404 / 400. The v0.8.x line additionally ships the `table_action` framework (per-table custom buttons surfaced in `TableList` / `TableSearch` / `TableEdit` / `TableDetail`); see the [Migrating to v0.7.0 §5 — TableAction](#migrating-to-v070--payout-user-payment-methods-table-actions) section for the seed shape (basis `table_action` + `authorization_object` + `authorization_object_action` rows) and the [keel/README Table Actions](https://github.com/nauticana/keel#table-actions) section for backend wiring via `handler.WrapTableAction`.
|
|
6
6
|
|
|
7
|
+
**Recent additions:** `BaseAuthService.acceptToken(jwt)` — adopt an externally-minted JWT (registration / SSO-handoff flows) and run the full post-login sequence (store under the canonical `jwt` key, load appdata, init routes); apps must use this instead of writing `localStorage` directly. `BaseRestService.analytic<T>(endpoint, params?)` — GET a keel `analytic/<endpoint>` report and return its rows.
|
|
8
|
+
|
|
7
9
|
## What it provides
|
|
8
10
|
|
|
9
11
|
| Category | Exports |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauticana/sail",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Metadata-driven Angular component library for CRUD admin frontends backed by the keel Go backend. Tables, forms, navigation, OTP/2FA/social auth, billing, payout onboarding.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -9,6 +9,7 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
|
9
9
|
import {
|
|
10
10
|
ConsentState, LoginResponseSocial, SignupConsent, SocialProvider,
|
|
11
11
|
} from '../../model/auth';
|
|
12
|
+
import { firstValueFrom } from 'rxjs';
|
|
12
13
|
|
|
13
14
|
declare const google: any;
|
|
14
15
|
declare const AppleID: any;
|
|
@@ -66,9 +67,11 @@ export class SocialLoginComponent extends BaseAsync implements OnInit {
|
|
|
66
67
|
|
|
67
68
|
private async initGoogle() {
|
|
68
69
|
await loadScript(GSI_URL);
|
|
70
|
+
const nonce = await firstValueFrom(this.auth.getSocialNonce());
|
|
69
71
|
google.accounts.id.initialize({
|
|
70
72
|
client_id: this.guiConfig.googleClientId,
|
|
71
73
|
callback: (res: { credential: string }) => this.onIdToken('google', res.credential),
|
|
74
|
+
...(nonce ? { nonce } : {}),
|
|
72
75
|
});
|
|
73
76
|
// Render into the template anchor once the view is ready.
|
|
74
77
|
queueMicrotask(() => {
|
|
@@ -80,11 +83,13 @@ export class SocialLoginComponent extends BaseAsync implements OnInit {
|
|
|
80
83
|
|
|
81
84
|
private async initApple() {
|
|
82
85
|
await loadScript(APPLE_URL);
|
|
86
|
+
const nonce = await firstValueFrom(this.auth.getSocialNonce());
|
|
83
87
|
AppleID.auth.init({
|
|
84
88
|
clientId: this.guiConfig.appleServiceId,
|
|
85
89
|
scope: 'name email',
|
|
86
90
|
redirectURI: this.guiConfig.appleRedirectUri ?? window.location.origin,
|
|
87
91
|
usePopup: true,
|
|
92
|
+
...(nonce ? { nonce } : {}),
|
|
88
93
|
});
|
|
89
94
|
}
|
|
90
95
|
|
package/src/config.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface SailGuiConfig {
|
|
|
66
66
|
* When set, login POSTs here before handing off to an allowed ?return= host. */
|
|
67
67
|
sessionCookieUrl?: string;
|
|
68
68
|
/** Hostnames a post-login `?return=` may redirect to (open-redirect allowlist),
|
|
69
|
-
* e.g. ['mcp.
|
|
69
|
+
* e.g. ['mcp.example.com']. Anything not listed is ignored. */
|
|
70
70
|
allowedReturnHosts?: string[];
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
UserProfile,
|
|
12
12
|
} from '../model/auth';
|
|
13
13
|
import { RestURL } from './rest_url';
|
|
14
|
-
import { Observable, ReplaySubject } from 'rxjs';
|
|
14
|
+
import { Observable, ReplaySubject, catchError, of } from 'rxjs';
|
|
15
15
|
import { ApplicationMenu, ConstantValue } from '../model/common';
|
|
16
16
|
import { CanActivateFn, CanDeactivateFn, Router, Routes } from '@angular/router';
|
|
17
17
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../config';
|
|
@@ -106,6 +106,13 @@ export abstract class BaseAuthService extends BaseRestService {
|
|
|
106
106
|
).subscribe((data: ApplicationData) => this.appData$.next(data));
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/** Adopt an externally-minted JWT (registration/SSO handoff flows) and run the
|
|
110
|
+
* full post-login sequence — store under the canonical key, load appdata, init
|
|
111
|
+
* routes. Apps must use this instead of writing localStorage directly. */
|
|
112
|
+
acceptToken(token: string) {
|
|
113
|
+
this.completeLogin(token);
|
|
114
|
+
}
|
|
115
|
+
|
|
109
116
|
private completeLogin(token: string) {
|
|
110
117
|
this.token = token;
|
|
111
118
|
this.isLoggedIn.set(true);
|
|
@@ -374,6 +381,16 @@ export abstract class BaseAuthService extends BaseRestService {
|
|
|
374
381
|
);
|
|
375
382
|
}
|
|
376
383
|
|
|
384
|
+
// Single-use nonce for the social id_token flow (replay protection): GET on
|
|
385
|
+
// the same social-login route. Empty when the backend hasn't enabled it —
|
|
386
|
+
// callers then sign in without one.
|
|
387
|
+
getSocialNonce(): Observable<string> {
|
|
388
|
+
return this.http.get<{ nonce: string }>(this.loginSocialUrl).pipe(
|
|
389
|
+
map((res) => res?.nonce ?? ''),
|
|
390
|
+
catchError(() => of('')),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
377
394
|
// ── Account lifecycle ──
|
|
378
395
|
|
|
379
396
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { HttpClient } from '@angular/common/http';
|
|
1
|
+
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
2
2
|
import { inject } from '@angular/core';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
3
4
|
import { RestURL } from './rest_url';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -17,4 +18,14 @@ export abstract class BaseRestService {
|
|
|
17
18
|
protected url(path: string): string {
|
|
18
19
|
return RestURL.httpHost + path;
|
|
19
20
|
}
|
|
21
|
+
|
|
22
|
+
/** GET an analytic endpoint (`{host}{api_prefix}analytic/<endpoint>`), returning
|
|
23
|
+
* its rows. Optional flat string params become the query string. */
|
|
24
|
+
protected analytic<T>(endpoint: string, params?: Record<string, string>): Observable<T[]> {
|
|
25
|
+
let p = new HttpParams();
|
|
26
|
+
for (const k in params) {
|
|
27
|
+
if (Object.prototype.hasOwnProperty.call(params, k)) p = p.set(k, params[k]);
|
|
28
|
+
}
|
|
29
|
+
return this.http.get<T[]>(this.url(RestURL.api_prefix + 'analytic/' + endpoint), { params: p });
|
|
30
|
+
}
|
|
20
31
|
}
|