@nauticana/sail 1.1.31 → 1.1.32

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
@@ -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.31 adopts keel v1.2.100 (tenant single sign-on, DNS and HTTP-file domain verification), sail v1.1.30 adopts keel v1.2.99 (password routes; a password change signs out), sail v1.1.29 adopts keel v1.2.94 (signup: confirmation signs in, partner setup, sign-in hand-off code), 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`.
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.32 adopts keel v1.2.101 (the OAuth connect callback ticket), sail v1.1.31 adopts keel v1.2.100 (tenant single sign-on, DNS and HTTP-file domain verification), sail v1.1.30 adopts keel v1.2.99 (password routes; a password change signs out), sail v1.1.29 adopts keel v1.2.94 (signup: confirmation signs in, partner setup, sign-in hand-off code), 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
 
@@ -1242,8 +1242,19 @@ catalog and any routing via the outputs.
1242
1242
  - Each `OAuthProviderField` has a **`role`**: `credential` (sealed secret), `endpoint` (stored
1243
1243
  as `api_endpoint`), or `param` (OAuth authorize query param) — so field order is
1244
1244
  irrelevant. `icon` is a Material name or an image URL.
1245
+ - **Connect flow (keel v1.2.101).** `startOAuth` sends the signed-in user to provider consent.
1246
+ keel's callback validates the response, parks it under a single-use ticket and redirects the
1247
+ browser to its `FrontendReturnURL` with `?connect=<provider>&ticket=<ticket>`. The panel on
1248
+ that page strips both parameters from the URL (hash preserved, so a reload never re-posts a
1249
+ spent ticket), posts the ticket with the user's bearer token to `/api/oauth/<provider>/complete`,
1250
+ then reloads and emits `connected` once the row is visible. A `connect` value that is not a
1251
+ configured provider is ignored. keel binds the connection to the user who started the flow
1252
+ (RFC 6749 §10.12): a 403 ("started by another user") and a 400 (expired or already used
1253
+ ticket) show distinct messages asking the user to start again. **The page at
1254
+ `FrontendReturnURL` must require sign-in before the panel runs.**
1245
1255
  - **`OAuthConnectionService`** — `startOAuth(provider, extra)` → consent URL,
1246
- `testConnection`, `list(entityId)` / `disconnect` (generic `partner_credential` REST), and
1256
+ `completeOAuth(provider, ticket)` → `{status, provider}`, `testConnection`,
1257
+ `list(entityId)` / `disconnect` (generic `partner_credential` REST), and
1247
1258
  `saveApiKey` (POSTs the sealed `/api/oauth/apikey`).
1248
1259
 
1249
1260
  ```html
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nauticana/sail",
3
- "version": "1.1.31",
3
+ "version": "1.1.32",
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": {
@@ -62,21 +62,38 @@ export class OAuthConnectionPanelComponent implements OnInit {
62
62
  });
63
63
  }
64
64
 
65
- // After an OAuth round-trip keel redirects back with ?<provider>=success. Record
66
- // the codes (configured providers only), strip them — preserving the hash — and
67
- // emit `connected` once the reload confirms the row exists (confirmSuccess).
65
+ // After the provider callback keel redirects back with ?connect=<provider>&ticket=…
66
+ // The ticket is single-use, so strip it (hash preserved) before redeeming it for
67
+ // the signed-in user; the reload then confirms the row exists (confirmSuccess).
68
68
  ngOnInit(): void {
69
69
  const q = new URLSearchParams(window.location.search);
70
- const known = new Set(this.providers().map((p) => p.code));
71
- const succeeded = new Set<string>();
72
- q.forEach((value, key) => {
73
- if (value === 'success' && known.has(key)) succeeded.add(key);
74
- });
75
- if (!succeeded.size) return;
76
- this.pendingSuccess.set(succeeded);
77
- for (const code of succeeded) q.delete(code);
70
+ const provider = q.get('connect') ?? '';
71
+ const ticket = q.get('ticket') ?? '';
72
+ if (!provider && !ticket) return;
73
+ q.delete('connect');
74
+ q.delete('ticket');
78
75
  const qs = q.toString();
79
76
  window.history.replaceState({}, '', window.location.pathname + (qs ? '?' + qs : '') + window.location.hash);
77
+ if (!ticket || !this.providers().some((p) => p.code === provider)) return;
78
+ this.busy.set(provider);
79
+ this.svc.completeOAuth(provider, ticket).subscribe({
80
+ next: () => {
81
+ this.busy.set('');
82
+ this.pendingSuccess.set(new Set([provider]));
83
+ this.reload(this.entityId());
84
+ },
85
+ error: (e) => {
86
+ this.busy.set('');
87
+ this.error.set(this.completeMsg(e));
88
+ },
89
+ });
90
+ }
91
+
92
+ private completeMsg(e: unknown): string {
93
+ const status = (e as { status?: number })?.status;
94
+ if (status === 403) return 'This connection was started by another user — start again from this account.';
95
+ if (status === 400) return 'The connection link expired — start again.';
96
+ return this.msg(e, 'Could not complete the connection.');
80
97
  }
81
98
 
82
99
  private reload(entityId: number): void {
@@ -24,6 +24,16 @@ export class OAuthConnectionService extends BaseRestService {
24
24
  return this.http.get<{ url: string }>(this.url(path)).pipe(map((r) => r.url));
25
25
  }
26
26
 
27
+ /** Finish an OAuth connect flow: redeem the single-use callback ticket keel put
28
+ * in `?connect=<provider>&ticket=...` for the signed-in user who started it.
29
+ * 400 = expired or spent ticket; 403 = started by another user or partner. */
30
+ completeOAuth(provider: string, ticket: string): Observable<{ status: string; provider: string }> {
31
+ return this.http.post<{ status: string; provider: string }>(
32
+ this.url(`/api/oauth/${encodeURIComponent(provider)}/complete`),
33
+ { ticket },
34
+ );
35
+ }
36
+
27
37
  /** Re-validate a stored connection (POST — it makes an outbound call and updates
28
38
  * status). entityId selects which one (0 = tenant-wide). */
29
39
  testConnection(provider: string, entityId = 0): Observable<{ status: string; message?: string }> {