@nauticana/sail 1.1.27 → 1.1.28
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 +6 -5
- package/package.json +1 -1
- package/src/service/auth.service.ts +31 -18
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A shared Angular component library for building CRUD-based admin frontends. Provides table management, form handling, navigation, authentication, two-factor authentication, and trusted device management — all driven by metadata from a [keel](https://github.com/nauticana/keel) Go backend.
|
|
4
4
|
|
|
5
|
-
> **Compatibility:** sail and keel are versioned in lock-step. The current line is **sail v1.1.x ↔ keel v1.2.x**; sail v1.1.18 adopts keel v1.2.58 (refresh-token rotation, `/public/logout`, setup intents, the realtime hub), the agency components in sail v1.1.14 require keel v1.2.41 (sail v1.1.9 needs keel v1.2.16 for the OAuth-connect layer). 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`.
|
|
5
|
+
> **Compatibility:** sail and keel are versioned in lock-step. The current line is **sail v1.1.x ↔ keel v1.2.x**; sail v1.1.28 adopts keel v1.2.93 (absolute `return` for the OAuth hand-off), sail v1.1.18 adopts keel v1.2.58 (refresh-token rotation, `/public/logout`, setup intents, the realtime hub), the agency components in sail v1.1.14 require keel v1.2.41 (sail v1.1.9 needs keel v1.2.16 for the OAuth-connect layer). 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
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. `passwordPolicyValidator` + `BaseAuthService.ensurePasswordPolicy()` — validate passwords against keel's policy (via `SailGuiConfig.passwordPolicyUrl`); see Configuration reference.
|
|
8
8
|
|
|
@@ -315,11 +315,12 @@ interface SailGuiConfig {
|
|
|
315
315
|
|
|
316
316
|
### Password-policy validation
|
|
317
317
|
|
|
318
|
-
**OAuth sign-in hand-off (v1.1.
|
|
319
|
-
`?return=<authorize URL>` and both `oauthServerUrl` and `allowedReturnHosts` are set,
|
|
320
|
-
|
|
318
|
+
**OAuth sign-in hand-off (v1.1.28, keel v1.2.93).** When the login page is opened with
|
|
319
|
+
`?return=<absolute authorize URL>` and both `oauthServerUrl` and `allowedReturnHosts` are set,
|
|
320
|
+
sail posts `{"return": <raw value>}` with the bearer JWT to
|
|
321
321
|
`{oauthServerUrl}/oauth/session/handoff` and then leaves the SPA with a full navigation to the
|
|
322
|
-
`redirect` keel answers.
|
|
322
|
+
`redirect` keel answers. This runs after a sign-in, and from `loadStoredSession()` when a user
|
|
323
|
+
who is already signed in opens a `/login` route; a 401 rotates the refresh token once and retries. keel sets the authorization server's own HttpOnly cookie while
|
|
323
324
|
redeeming that URL; sail does not opt into cross-origin cookies or send a CSRF token. A
|
|
324
325
|
`return` whose host is not in `allowedReturnHosts`, or a `redirect` outside `oauthServerUrl`'s
|
|
325
326
|
origin, is not followed. Failures set `BaseAuthService.sessionHandoffError` (rendered by
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauticana/sail",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.28",
|
|
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
|
"exports": {
|
|
@@ -25,7 +25,7 @@ export class RefreshSupersededError extends Error {
|
|
|
25
25
|
constructor() { super('session changed during refresh'); this.name = 'RefreshSupersededError'; }
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function handoffErrorMessage(status: number): string {
|
|
28
|
+
function handoffErrorMessage(status: number | undefined): string {
|
|
29
29
|
if (status === 400) return 'This sign-in link is not valid. Start again from the application you were connecting.';
|
|
30
30
|
if (status === 401) return 'Your session was not accepted. Please sign in again.';
|
|
31
31
|
return 'Could not complete sign-in. Please try again.';
|
|
@@ -219,25 +219,37 @@ export abstract class BaseAuthService extends BaseRestService {
|
|
|
219
219
|
this.isLoggedIn.set(true);
|
|
220
220
|
this.sessionHandoffError.set(null);
|
|
221
221
|
resetAuthCircuit(); // a new session must not inherit a prior session's open circuit
|
|
222
|
-
|
|
223
|
-
const oauthServer = this.oauthServerBase();
|
|
224
|
-
if (ret && oauthServer) {
|
|
225
|
-
this.handOffSession(oauthServer, ret, token);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
222
|
+
if (this.startHandoff()) return;
|
|
228
223
|
this.loadAppData();
|
|
229
224
|
this.initRoutes();
|
|
230
225
|
}
|
|
231
226
|
|
|
227
|
+
/** Hands the session to the OAuth authorization server when the page carries an
|
|
228
|
+
* allowed `?return=` and oauthServerUrl is set; false when there is nothing to hand off. */
|
|
229
|
+
private startHandoff(): boolean {
|
|
230
|
+
const ret = this.validatedReturnUrl();
|
|
231
|
+
const oauthServer = this.oauthServerBase();
|
|
232
|
+
if (!ret || !oauthServer) return false;
|
|
233
|
+
this.handOffSession(oauthServer, ret);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
232
237
|
/** Exchange the bearer session for the authorization server's own cookie session
|
|
233
238
|
* (keel POST /oauth/session/handoff), then leave the SPA for the URL it returns.
|
|
234
|
-
*
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
* A 401 rotates the refresh token once and retries. Any failure stays on the
|
|
240
|
+
* login page; the browser is never sent elsewhere. */
|
|
241
|
+
private handOffSession(oauthServer: string, ret: string): void {
|
|
242
|
+
const mint = (token: string) => this.http.post<{ data?: { redirect?: unknown } }>(
|
|
237
243
|
oauthServer + '/oauth/session/handoff',
|
|
238
244
|
{ return: ret },
|
|
239
245
|
{ headers: { Authorization: `Bearer ${token}` } },
|
|
240
|
-
)
|
|
246
|
+
);
|
|
247
|
+
mint(this.token ?? '').pipe(
|
|
248
|
+
catchError((err: HttpErrorResponse) => err.status === 401 && localStorage.getItem('refreshToken')
|
|
249
|
+
? this.refreshSession().pipe(mergeMap(mint))
|
|
250
|
+
: throwError(() => err)),
|
|
251
|
+
take(1),
|
|
252
|
+
).subscribe({
|
|
241
253
|
next: (res) => {
|
|
242
254
|
const redirect = res?.data?.redirect;
|
|
243
255
|
if (typeof redirect !== 'string' || !sameOrigin(redirect, oauthServer)) {
|
|
@@ -247,9 +259,9 @@ export abstract class BaseAuthService extends BaseRestService {
|
|
|
247
259
|
}
|
|
248
260
|
window.location.assign(redirect);
|
|
249
261
|
},
|
|
250
|
-
error: (err:
|
|
262
|
+
error: (err: unknown) => {
|
|
251
263
|
console.error('Session handoff failed:', err);
|
|
252
|
-
this.sessionHandoffError.set(handoffErrorMessage(err
|
|
264
|
+
this.sessionHandoffError.set(handoffErrorMessage((err as { status?: number } | null)?.status));
|
|
253
265
|
},
|
|
254
266
|
});
|
|
255
267
|
}
|
|
@@ -448,11 +460,12 @@ export abstract class BaseAuthService extends BaseRestService {
|
|
|
448
460
|
|
|
449
461
|
loadStoredSession() {
|
|
450
462
|
this.token = localStorage.getItem('jwt');
|
|
451
|
-
if (this.token)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
463
|
+
if (!this.token) return;
|
|
464
|
+
this.isLoggedIn.set(true);
|
|
465
|
+
// A signed-in user sent to the login page by the authorization server is handed off without signing in again.
|
|
466
|
+
if (window.location.pathname.startsWith('/login') && this.startHandoff()) return;
|
|
467
|
+
this.loadAppData();
|
|
468
|
+
this.initRoutes();
|
|
456
469
|
}
|
|
457
470
|
|
|
458
471
|
/** Revoke the refresh token server-side (best effort: the local session is
|