@nauticana/sail 1.1.29 → 1.1.30

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.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.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
 
@@ -350,6 +350,9 @@ Omit the URL to skip client-side policy checks. The server stays the authoritati
350
350
  | `POST /public/register/confirm` | Confirm `?email=&code=`; answers session tokens and the partner result (`confirmRegister`) |
351
351
  | `POST /public/register/exchange` | Trade a single-use hand-off `{code}` for a session (`exchangeHandoff`) |
352
352
  | `POST /api/register/partner` | Create the signed-in user's partner from a `PartnerSetup` (`createPartner`) |
353
+ | `POST /public/password/change` | Change `{username, old_password, new_password}` (`chpass`); keel revokes every refresh token |
354
+ | `POST /public/password/forgot` | Email a reset code for `{username}` (`forgotPassword`) |
355
+ | `POST /public/password/reset` | Set the new password from `{username, code, new_password}` (`confirmChpass`); revokes every refresh token |
353
356
  | `POST /public/token/refresh` | Rotate `{refreshToken}` → new `{token, refreshToken}`; 401 ends the session |
354
357
  | `POST /public/logout` | Revoke `{refreshToken}` (called by `BaseAuthService.logout()`) |
355
358
  | `GET /public/ws` | keel realtime hub handshake (`RealtimeService`; JWT via `?token=`) |
@@ -417,6 +420,8 @@ The session JWT, the refresh token, and the short-lived `loginToken` are kept in
417
420
 
418
421
  Every keel login response (keel v1.2.57+) carries `refreshToken` beside `token`; `completeLogin` stores both. When an authenticated call answers 401, `authInterceptor` calls `BaseAuthService.refreshSession()` — one shared `POST /public/token/refresh` per burst — stores the rotated pair and replays the request with the new JWT. A 401 from the refresh itself (revoked, expired, already rotated) clears the session and lands on `login?sessionExpired=true`; any other refresh failure keeps the session and lets the 401 count toward the circuit breaker. `logout()` posts the refresh token to `/public/logout` before clearing local state. `acceptToken(token, refreshToken?)` accepts the pair for handoff flows. No wiring beyond registering `authInterceptor`.
419
422
 
423
+ A successful `chpass()` or `confirmChpass()` clears any current session and lands on `login?passwordChanged=true`, where `<sail-login>` asks the user to sign in with the new password.
424
+
420
425
  ### Realtime
421
426
 
422
427
  `RealtimeService` (root-provided) is the client for keel's `realtime.Hub`. `connect()` opens `RestURL.realtimeURL` (`/public/ws`) on the configured backend host with the stored JWT, reconnects with backoff, and re-subscribes every channel after a reconnect. `channel<T>(name)` returns the payloads keel broadcasts on that channel; `userFrames<T>()` returns payloads addressed to the user, including keel's `{op: "notification"}` frames; `send(frame)` forwards an application frame to the hub's `OnMessage`. A subscription keel's `CanSubscribe` rejects is logged and exposed on `lastError`. Channel names and payload shapes are app-owned.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nauticana/sail",
3
- "version": "1.1.29",
3
+ "version": "1.1.30",
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": {
@@ -43,10 +43,6 @@
43
43
  <div class="auth-error" role="alert">{{ errorMessage() }}</div>
44
44
  }
45
45
 
46
- @if (successMessage()) {
47
- <div class="auth-success" role="status">{{ successMessage() }}</div>
48
- }
49
-
50
46
  <button matButton="elevated" class="primary" type="submit" [disabled]="!chpassForm.valid">
51
47
  @if (forgotPassword()) {
52
48
  Send Confirmation Code
@@ -90,11 +90,7 @@ export class ChpassComponent extends BaseAsync {
90
90
  this.run(
91
91
  request,
92
92
  () => {
93
- if (isForgot) {
94
- this.router.navigate(['/confirm/password'], { queryParams: { username } });
95
- } else {
96
- this.successMessage.set('Password changed successfully.');
97
- }
93
+ if (isForgot) this.router.navigate(['/confirm/password'], { queryParams: { username } });
98
94
  },
99
95
  'Password change failed. Please try again.',
100
96
  );
@@ -36,10 +36,6 @@
36
36
  <div class="auth-error" role="alert">{{ errorMessage() }}</div>
37
37
  }
38
38
 
39
- @if (successMessage()) {
40
- <div class="auth-success" role="status">{{ successMessage() }}</div>
41
- }
42
-
43
39
  <button matButton="elevated" class="primary" type="submit" [disabled]="!confirmForm.valid">
44
40
  Confirm Password Change
45
41
  </button>
@@ -1,7 +1,7 @@
1
1
  import { ChangeDetectionStrategy, Component, computed, effect, inject, OnInit, ViewEncapsulation } from '@angular/core';
2
2
  import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
3
3
  import { BaseAuthService } from '../../service/auth.service';
4
- import { ActivatedRoute, Router, RouterLink } from '@angular/router';
4
+ import { ActivatedRoute, RouterLink } from '@angular/router';
5
5
  import { MatButtonModule } from '@angular/material/button';
6
6
  import { MatFormFieldModule } from '@angular/material/form-field';
7
7
  import { MatInputModule } from '@angular/material/input';
@@ -25,7 +25,6 @@ import { passwordPolicyValidator, passwordPolicyHint } from '../../util/password
25
25
  export class ConfirmChpassComponent extends BaseAsync implements OnInit {
26
26
  private auth = inject(BaseAuthService);
27
27
  private fb = inject(FormBuilder);
28
- private router = inject(Router);
29
28
  private route = inject(ActivatedRoute);
30
29
  protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
31
30
 
@@ -67,7 +66,7 @@ export class ConfirmChpassComponent extends BaseAsync implements OnInit {
67
66
 
68
67
  this.run(
69
68
  this.auth.confirmChpass(username!, code!, new_password!),
70
- () => this.router.navigate(['/login/local']),
69
+ () => {},
71
70
  'Confirmation failed. Please try again.',
72
71
  );
73
72
  }
@@ -9,6 +9,9 @@
9
9
  @if (loggedOut) {
10
10
  <p class="auth-message" role="status" aria-live="polite">You have been logged out successfully.</p>
11
11
  }
12
+ @if (passwordChanged) {
13
+ <p class="auth-message" role="status" aria-live="polite">Your password was changed. Sign in again with the new password.</p>
14
+ }
12
15
  @if (sessionHandoffError()) {
13
16
  <p class="auth-error" role="alert">{{ sessionHandoffError() }}</p>
14
17
  }
@@ -28,7 +28,9 @@ export class LoginComponent extends BaseAsync {
28
28
  private auth = inject(BaseAuthService);
29
29
  protected fb = inject(FormBuilder);
30
30
  protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
31
- protected readonly loggedOut = inject(ActivatedRoute).snapshot.queryParamMap.get('loggedOut') === 'true';
31
+ private readonly queryParams = inject(ActivatedRoute).snapshot.queryParamMap;
32
+ protected readonly loggedOut = this.queryParams.get('loggedOut') === 'true';
33
+ protected readonly passwordChanged = this.queryParams.get('passwordChanged') === 'true';
32
34
  /** OAuth session hand-off failure from BaseAuthService, shown below. */
33
35
  protected readonly sessionHandoffError = this.auth.sessionHandoffError;
34
36
 
@@ -89,6 +89,7 @@ export abstract class BaseAuthService extends BaseRestService {
89
89
  protected get logoutUrl() { return this.url(RestURL.logoutURL); }
90
90
  protected get registerUrl() { return this.url(RestURL.registerURL); }
91
91
  protected get chpassUrl() { return this.url(RestURL.chpassURL); }
92
+ protected get forgotPasswordUrl() { return this.url(RestURL.forgotPasswordURL); }
92
93
  protected get confirmRegisterUrl() { return this.url(RestURL.confirmRegisterURL); }
93
94
  protected get registerExchangeUrl() { return this.url(RestURL.registerExchangeURL); }
94
95
  protected get registerPartnerUrl() { return this.url(RestURL.registerPartnerURL); }
@@ -416,11 +417,12 @@ export abstract class BaseAuthService extends BaseRestService {
416
417
  }
417
418
 
418
419
  chpass(username: string, new_password: string, old_password: string) {
419
- return this.http.post<{ message: string }>(this.chpassUrl, { username, old_password, new_password });
420
+ return this.http.post<{ status: string }>(this.chpassUrl, { username, old_password, new_password })
421
+ .pipe(tap(() => this.passwordChanged()));
420
422
  }
421
423
 
422
424
  forgotPassword(username: string) {
423
- return this.http.post<{ message: string }>(this.chpassUrl, { username });
425
+ return this.http.post<{ status: string }>(this.forgotPasswordUrl, { username });
424
426
  }
425
427
 
426
428
  // ── Self-service profile (keel ProfileHandler) ──
@@ -480,7 +482,8 @@ export abstract class BaseAuthService extends BaseRestService {
480
482
  }
481
483
 
482
484
  confirmChpass(username: string, code: string, new_password: string) {
483
- return this.http.post<{ message: string }>(this.confirmChpassUrl, { username, code, new_password });
485
+ return this.http.post<{ status: string }>(this.confirmChpassUrl, { username, code, new_password })
486
+ .pipe(tap(() => this.passwordChanged()));
484
487
  }
485
488
 
486
489
  loadStoredSession() {
@@ -653,6 +656,11 @@ export abstract class BaseAuthService extends BaseRestService {
653
656
  this.router.navigate(['/login/local'], { queryParams: { sessionExpired: 'true' } });
654
657
  }
655
658
 
659
+ private passwordChanged(): void {
660
+ this.clearSession();
661
+ this.router.navigate(['/login/local'], { queryParams: { passwordChanged: 'true' } });
662
+ }
663
+
656
664
  private buildAuthIndex() {
657
665
  this.authIndex.clear();
658
666
  if (!this.cache?.Permissions) return;
@@ -8,12 +8,13 @@ export const RestURL = {
8
8
  realtimeURL: '/public/ws', // keel realtime.Hub handshake; JWT via ?token=
9
9
  loginGoogleURL: '/public/login/google',
10
10
  registerURL: '/public/register',
11
- chpassURL: '/public/login/chpass',
11
+ chpassURL: '/public/password/change', // keel revokes every refresh token on success
12
+ forgotPasswordURL: '/public/password/forgot',
12
13
  confirmRegisterURL: '/public/register/confirm',
13
14
  registerExchangeURL: '/public/register/exchange',
14
15
  registerPartnerURL: '/api/register/partner',
15
16
  plansURL: '/public/plans',
16
- confirmChpassURL: '/public/confirm/password',
17
+ confirmChpassURL: '/public/password/reset',
17
18
  twoFactorSetupURL: '/api/user/2fa/setup',
18
19
  twoFactorVerifyURL: '/api/user/2fa/verify', // authenticated — confirms setup
19
20
  twoFactorDisableURL: '/api/user/2fa/disable',