@nauticana/sail 1.1.2 → 1.1.4
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 +2 -0
- package/package.json +1 -1
- package/src/config.ts +9 -0
- package/src/service/auth.service.ts +37 -0
- package/src/service/base_rest.service.ts +12 -1
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.4",
|
|
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": {
|
package/src/config.ts
CHANGED
|
@@ -59,6 +59,15 @@ export interface SailGuiConfig {
|
|
|
59
59
|
defaultPolicyLanguage?: string;
|
|
60
60
|
/** Route to navigate to after account deletion. Defaults to '/login/local'. */
|
|
61
61
|
accountDeletedRoute?: string;
|
|
62
|
+
|
|
63
|
+
// ── OAuth authorization-flow bridge (for an OAuth AS on a different host) ──
|
|
64
|
+
/** Backend path that mirrors the bearer JWT into an HttpOnly session cookie so
|
|
65
|
+
* a cross-site OAuth `/authorize` can see the session, e.g. '/api/v1/auth/session-cookie'.
|
|
66
|
+
* When set, login POSTs here before handing off to an allowed ?return= host. */
|
|
67
|
+
sessionCookieUrl?: string;
|
|
68
|
+
/** Hostnames a post-login `?return=` may redirect to (open-redirect allowlist),
|
|
69
|
+
* e.g. ['mcp.example.com']. Anything not listed is ignored. */
|
|
70
|
+
allowedReturnHosts?: string[];
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
export const SAIL_GUI_CONFIG = new InjectionToken<SailGuiConfig>('SailGuiConfig');
|
|
@@ -106,14 +106,51 @@ 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);
|
|
112
119
|
localStorage.setItem('jwt', token);
|
|
120
|
+
const ret = this.validatedReturnUrl();
|
|
121
|
+
if (ret && this.guiConfig.sessionCookieUrl) {
|
|
122
|
+
// OAuth flow: mirror the bearer into a cookie, then hand off to the
|
|
123
|
+
// (cross-origin) authorization server via a full navigation.
|
|
124
|
+
this.http.post(this.url(this.guiConfig.sessionCookieUrl), {}, { withCredentials: true })
|
|
125
|
+
.pipe(take(1))
|
|
126
|
+
.subscribe({
|
|
127
|
+
next: () => window.location.assign(ret),
|
|
128
|
+
error: () => window.location.assign(ret),
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
113
132
|
this.loadAppData();
|
|
114
133
|
this.initRoutes();
|
|
115
134
|
}
|
|
116
135
|
|
|
136
|
+
/** A post-login `?return=` URL, only when its host is in allowedReturnHosts and
|
|
137
|
+
* the scheme is https (or loopback) — an open-redirect guard. '' otherwise. */
|
|
138
|
+
private validatedReturnUrl(): string {
|
|
139
|
+
const raw = new URLSearchParams(window.location.search).get('return');
|
|
140
|
+
if (!raw) return '';
|
|
141
|
+
try {
|
|
142
|
+
const u = new URL(raw, window.location.origin);
|
|
143
|
+
if (u.username || u.password) return ''; // reject userinfo ("user@host") spoofing
|
|
144
|
+
const host = u.hostname.toLowerCase();
|
|
145
|
+
const okScheme = u.protocol === 'https:' || host === 'localhost' || host === '127.0.0.1';
|
|
146
|
+
const allowed = (this.guiConfig.allowedReturnHosts ?? []).some((h) => h.toLowerCase() === host);
|
|
147
|
+
if (okScheme && allowed) {
|
|
148
|
+
return u.toString();
|
|
149
|
+
}
|
|
150
|
+
} catch { /* malformed → reject */ }
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
|
|
117
154
|
login(username: string, password: string) {
|
|
118
155
|
// withCredentials so the browser sends the server-set `keel_td` cookie
|
|
119
156
|
// (keel v0.9 trusted-device secret). When it maps to a trusted row, keel
|
|
@@ -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
|
}
|