@nauticana/sail 1.1.28 → 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 +13 -2
- package/package.json +1 -1
- package/src/component/login/chpass_component.html +0 -4
- package/src/component/login/chpass_component.ts +1 -5
- package/src/component/login/confirm_chpass_component.html +0 -4
- package/src/component/login/confirm_chpass_component.ts +2 -3
- package/src/component/login/confirm_register_component.ts +3 -8
- package/src/component/login/login_component.html +3 -0
- package/src/component/login/login_component.ts +3 -1
- package/src/component/login/register_component.ts +16 -16
- package/src/model/appdata.ts +47 -27
- package/src/service/auth.service.ts +40 -7
- package/src/service/rest_url.ts +6 -3
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.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
|
|
|
@@ -346,6 +346,13 @@ Omit the URL to skip client-side policy checks. The server stays the authoritati
|
|
|
346
346
|
| `POST /public/otp/resend` | Re-issue OTP for an existing `otpToken` |
|
|
347
347
|
| `POST /public/2fa/verify` | Login-time TOTP verification (uses `loginToken`) |
|
|
348
348
|
| `POST /public/2fa/backup-verify` | Login-time backup-code verification |
|
|
349
|
+
| `POST /public/register` | Start a signup from a `PartnerRegistration`; emails the confirmation code |
|
|
350
|
+
| `POST /public/register/confirm` | Confirm `?email=&code=`; answers session tokens and the partner result (`confirmRegister`) |
|
|
351
|
+
| `POST /public/register/exchange` | Trade a single-use hand-off `{code}` for a session (`exchangeHandoff`) |
|
|
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 |
|
|
349
356
|
| `POST /public/token/refresh` | Rotate `{refreshToken}` → new `{token, refreshToken}`; 401 ends the session |
|
|
350
357
|
| `POST /public/logout` | Revoke `{refreshToken}` (called by `BaseAuthService.logout()`) |
|
|
351
358
|
| `GET /public/ws` | keel realtime hub handshake (`RealtimeService`; JWT via `?token=`) |
|
|
@@ -413,6 +420,8 @@ The session JWT, the refresh token, and the short-lived `loginToken` are kept in
|
|
|
413
420
|
|
|
414
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`.
|
|
415
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
|
+
|
|
416
425
|
### Realtime
|
|
417
426
|
|
|
418
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.
|
|
@@ -512,7 +521,9 @@ metadata: {
|
|
|
512
521
|
|
|
513
522
|
### Registration → checkout flow
|
|
514
523
|
|
|
515
|
-
|
|
524
|
+
`BaseAuthService.confirmRegister(email, code)` signs the user in from the tokens keel answers (keel v1.2.94), so no separate login follows. `ConfirmRegisterComponent` then sends the user to `resp.paymentUrl` when the backend returns `paymentRequired: true` with one; otherwise the post-login navigation applies.
|
|
525
|
+
|
|
526
|
+
A two-step signup registers the account alone (a `PartnerRegistration` without partner fields), then calls `createPartner(setup)` once signed in. It posts keel's `PartnerSetup` — application fields ride in `extra` — rotates the refresh token so the session carries the new partner, reloads appdata and routes, and emits `PartnerCreated`. When `paymentRequired` comes without a `paymentUrl`, the application's billing page opens the checkout. `exchangeHandoff(code)` completes a sign-in that arrived by redirect with keel's single-use code, including the 2FA step.
|
|
516
527
|
|
|
517
528
|
### `BaseAsync`
|
|
518
529
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauticana/sail",
|
|
3
|
-
"version": "1.1.
|
|
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,
|
|
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
|
-
() =>
|
|
69
|
+
() => {},
|
|
71
70
|
'Confirmation failed. Please try again.',
|
|
72
71
|
);
|
|
73
72
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ChangeDetectionStrategy, Component, 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,
|
|
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';
|
|
@@ -24,7 +24,6 @@ import { BaseAsync } from '../abstract/base_async';
|
|
|
24
24
|
export class ConfirmRegisterComponent extends BaseAsync implements OnInit {
|
|
25
25
|
private auth = inject(BaseAuthService);
|
|
26
26
|
private fb = inject(FormBuilder);
|
|
27
|
-
private router = inject(Router);
|
|
28
27
|
private route = inject(ActivatedRoute);
|
|
29
28
|
protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
|
|
30
29
|
|
|
@@ -49,15 +48,11 @@ export class ConfirmRegisterComponent extends BaseAsync implements OnInit {
|
|
|
49
48
|
this.auth.confirmRegister(email!, code!),
|
|
50
49
|
(resp) => {
|
|
51
50
|
this.successMessage.set('Registration confirmed.');
|
|
51
|
+
// confirmRegister signed the user in and owns navigation; only a paid
|
|
52
|
+
// plan's checkout leaves the SPA (window.location: it is cross-origin).
|
|
52
53
|
if (resp?.paymentRequired && resp.paymentUrl) {
|
|
53
|
-
// Paid plan: go straight to payment. Subscription is 'P' until paid.
|
|
54
|
-
// Using window.location (not router) so absolute / cross-origin
|
|
55
|
-
// payment URLs also work.
|
|
56
54
|
window.location.href = resp.paymentUrl;
|
|
57
|
-
return;
|
|
58
55
|
}
|
|
59
|
-
// Free plan: land on login.
|
|
60
|
-
this.router.navigate(['/login/local']);
|
|
61
56
|
},
|
|
62
57
|
'Confirmation failed. Please try again.',
|
|
63
58
|
);
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -159,22 +159,22 @@ export class RegisterComponent implements OnInit {
|
|
|
159
159
|
this.registerError.set('');
|
|
160
160
|
const formValue = this.registerForm.getRawValue();
|
|
161
161
|
const partnerReg: PartnerRegistration = {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
162
|
+
firstName: formValue.FirstName!,
|
|
163
|
+
lastName: formValue.LastName!,
|
|
164
|
+
userName: formValue.UserName!,
|
|
165
|
+
email: formValue.Email!,
|
|
166
|
+
password: formValue.Password!,
|
|
167
|
+
partnerCaption: formValue.PartnerCaption!,
|
|
168
|
+
address: formValue.Address!,
|
|
169
|
+
city: formValue.City!,
|
|
170
|
+
state: formValue.State!,
|
|
171
|
+
zipcode: formValue.Zipcode!,
|
|
172
|
+
country: formValue.Country!,
|
|
173
|
+
phone: formValue.Phone!,
|
|
174
|
+
domainUrl: formValue.DomainURL!,
|
|
175
|
+
latitude: formValue.Latitude ?? undefined,
|
|
176
|
+
longitude: formValue.Longitude ?? undefined,
|
|
177
|
+
planId: formValue.PlanID!,
|
|
178
178
|
};
|
|
179
179
|
this.auth.register(partnerReg).subscribe({
|
|
180
180
|
// Pass the email along so the confirm page pre-fills it.
|
package/src/model/appdata.ts
CHANGED
|
@@ -198,24 +198,46 @@ export interface TrustedDevice {
|
|
|
198
198
|
created_at: string;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
201
|
+
// Mirrors keel `user.AccountRegistration`.
|
|
202
|
+
export interface AccountRegistration {
|
|
203
|
+
firstName: string;
|
|
204
|
+
lastName: string;
|
|
205
|
+
userName: string;
|
|
206
|
+
email: string;
|
|
207
|
+
password: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Mirrors keel `user.PartnerSetup`. `extra` carries the application's own fields.
|
|
211
|
+
export interface PartnerSetup<TExtra = unknown> {
|
|
212
|
+
partnerCaption: string;
|
|
213
|
+
address: string;
|
|
214
|
+
city: string;
|
|
215
|
+
state: string;
|
|
216
|
+
zipcode: string;
|
|
217
|
+
country: string;
|
|
218
|
+
phone: string;
|
|
219
|
+
latitude?: number;
|
|
220
|
+
longitude?: number;
|
|
221
|
+
domainUrl: string;
|
|
222
|
+
planId: string;
|
|
223
|
+
// Chosen offer (PERIOD_TYPE codes); none selects the plan's cheapest.
|
|
224
|
+
billingCycle?: string;
|
|
225
|
+
termType?: string;
|
|
226
|
+
termCount?: number;
|
|
227
|
+
extra?: TExtra;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Mirrors keel `user.PartnerRegistration`. Without partner fields only the
|
|
231
|
+
// account is created and the partner follows through `createPartner`.
|
|
232
|
+
export type PartnerRegistration<TExtra = unknown> = AccountRegistration & Partial<PartnerSetup<TExtra>>;
|
|
233
|
+
|
|
234
|
+
// Mirrors keel `user.PartnerCreated`. `paymentRequired` without a `paymentUrl`
|
|
235
|
+
// means the application's billing page opens the checkout.
|
|
236
|
+
export interface PartnerCreated {
|
|
237
|
+
partnerId: number;
|
|
238
|
+
planId: string;
|
|
239
|
+
paymentRequired: boolean;
|
|
240
|
+
paymentUrl?: string;
|
|
219
241
|
}
|
|
220
242
|
|
|
221
243
|
// One purchasable offer for a plan — a (billing cycle × commitment term) row of
|
|
@@ -261,15 +283,13 @@ export interface PublicPlan {
|
|
|
261
283
|
trialDays?: number;
|
|
262
284
|
}
|
|
263
285
|
|
|
264
|
-
// ConfirmRegisterResponse — shape returned by POST /public/confirm
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
paymentRequired: boolean;
|
|
272
|
-
paymentUrl?: string;
|
|
286
|
+
// ConfirmRegisterResponse — shape returned by POST /public/register/confirm:
|
|
287
|
+
// the new session's tokens, plus the partner result when the registration had one.
|
|
288
|
+
export interface ConfirmRegisterResponse extends TokenPair {
|
|
289
|
+
status: string;
|
|
290
|
+
planId?: string;
|
|
291
|
+
paymentRequired?: boolean;
|
|
292
|
+
paymentUrl?: string;
|
|
273
293
|
}
|
|
274
294
|
|
|
275
295
|
// ── Billing / Payment types ──
|
|
@@ -2,7 +2,7 @@ import { Injectable, WritableSignal, inject, signal } from '@angular/core';
|
|
|
2
2
|
import { HttpErrorResponse, HttpParams } from '@angular/common/http';
|
|
3
3
|
import { filter, finalize, map, mergeMap, shareReplay, take, tap } from 'rxjs/operators';
|
|
4
4
|
import { PasswordRules } from '../util/password_policy';
|
|
5
|
-
import { ApplicationData, ConfirmRegisterResponse, DictionaryPath, LoginResponse2FA, PartnerRegistration, RestReport, TableDefinition, TokenPair, TrustedDevice, TwoFactorSetupResponse, TwoFactorVerifyRequest, TwoFactorVerifyResponse } from '../model/appdata';
|
|
5
|
+
import { ApplicationData, ConfirmRegisterResponse, DictionaryPath, LoginResponse2FA, PartnerCreated, PartnerRegistration, PartnerSetup, RestReport, TableDefinition, TokenPair, TrustedDevice, TwoFactorSetupResponse, TwoFactorVerifyRequest, TwoFactorVerifyResponse } from '../model/appdata';
|
|
6
6
|
import {
|
|
7
7
|
LoginResponseSocial,
|
|
8
8
|
OtpRequest, OtpResendRequest, OtpResponse, OtpVerifyRequest, OtpVerifyResponse,
|
|
@@ -89,7 +89,10 @@ 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); }
|
|
94
|
+
protected get registerExchangeUrl() { return this.url(RestURL.registerExchangeURL); }
|
|
95
|
+
protected get registerPartnerUrl() { return this.url(RestURL.registerPartnerURL); }
|
|
93
96
|
protected get confirmChpassUrl() { return this.url(RestURL.confirmChpassURL); }
|
|
94
97
|
protected get loginGoogleUrl() { return this.url(RestURL.loginGoogleURL); }
|
|
95
98
|
protected get twoFactorSetupUrl() { return this.url(RestURL.twoFactorSetupURL); }
|
|
@@ -414,11 +417,12 @@ export abstract class BaseAuthService extends BaseRestService {
|
|
|
414
417
|
}
|
|
415
418
|
|
|
416
419
|
chpass(username: string, new_password: string, old_password: string) {
|
|
417
|
-
return this.http.post<{
|
|
420
|
+
return this.http.post<{ status: string }>(this.chpassUrl, { username, old_password, new_password })
|
|
421
|
+
.pipe(tap(() => this.passwordChanged()));
|
|
418
422
|
}
|
|
419
423
|
|
|
420
424
|
forgotPassword(username: string) {
|
|
421
|
-
return this.http.post<{
|
|
425
|
+
return this.http.post<{ status: string }>(this.forgotPasswordUrl, { username });
|
|
422
426
|
}
|
|
423
427
|
|
|
424
428
|
// ── Self-service profile (keel ProfileHandler) ──
|
|
@@ -449,13 +453,37 @@ export abstract class BaseAuthService extends BaseRestService {
|
|
|
449
453
|
return this.http.post<{ message: string }>(this.profilePhoneConfirmUrl, { value, code });
|
|
450
454
|
}
|
|
451
455
|
|
|
452
|
-
|
|
456
|
+
/** Confirm a registration; success completes the login from the tokens keel
|
|
457
|
+
* answers. A registration without partner fields leaves `partnerId` 0. */
|
|
458
|
+
confirmRegister(email: string, code: string): Observable<ConfirmRegisterResponse> {
|
|
453
459
|
const params = new HttpParams().set('email', email).set('code', code);
|
|
454
|
-
return this.http.post<ConfirmRegisterResponse>(this.confirmRegisterUrl, null, { params })
|
|
460
|
+
return this.http.post<ConfirmRegisterResponse>(this.confirmRegisterUrl, null, { params }).pipe(
|
|
461
|
+
tap((res) => this.completeLogin(res.token, res.refreshToken)),
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Create the partner of a signed-in user who has none, then rotate the
|
|
466
|
+
* refresh token so the session carries the partner. An error after keel
|
|
467
|
+
* created the partner means only the rotation failed; do not post again. */
|
|
468
|
+
createPartner(setup: PartnerSetup): Observable<PartnerCreated> {
|
|
469
|
+
return this.http.post<PartnerCreated>(this.registerPartnerUrl, setup).pipe(
|
|
470
|
+
mergeMap((created) => this.refreshSession().pipe(
|
|
471
|
+
tap((token) => this.completeLogin(token, localStorage.getItem('refreshToken') ?? undefined)),
|
|
472
|
+
map(() => created),
|
|
473
|
+
)),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Trade keel's single-use hand-off code for a session; same contract as login(). */
|
|
478
|
+
exchangeHandoff(code: string): Observable<LoginResponse2FA> {
|
|
479
|
+
return this.http.post<LoginResponse2FA>(this.registerExchangeUrl, { code }, { withCredentials: true }).pipe(
|
|
480
|
+
tap((res) => this.handleLoginResponse(res)),
|
|
481
|
+
);
|
|
455
482
|
}
|
|
456
483
|
|
|
457
484
|
confirmChpass(username: string, code: string, new_password: string) {
|
|
458
|
-
return this.http.post<{
|
|
485
|
+
return this.http.post<{ status: string }>(this.confirmChpassUrl, { username, code, new_password })
|
|
486
|
+
.pipe(tap(() => this.passwordChanged()));
|
|
459
487
|
}
|
|
460
488
|
|
|
461
489
|
loadStoredSession() {
|
|
@@ -485,7 +513,7 @@ export abstract class BaseAuthService extends BaseRestService {
|
|
|
485
513
|
}
|
|
486
514
|
|
|
487
515
|
register(reg: PartnerRegistration) {
|
|
488
|
-
return this.http.post<
|
|
516
|
+
return this.http.post<{ status: string }>(this.registerUrl, reg);
|
|
489
517
|
}
|
|
490
518
|
|
|
491
519
|
// ── Phone / email OTP ──
|
|
@@ -628,6 +656,11 @@ export abstract class BaseAuthService extends BaseRestService {
|
|
|
628
656
|
this.router.navigate(['/login/local'], { queryParams: { sessionExpired: 'true' } });
|
|
629
657
|
}
|
|
630
658
|
|
|
659
|
+
private passwordChanged(): void {
|
|
660
|
+
this.clearSession();
|
|
661
|
+
this.router.navigate(['/login/local'], { queryParams: { passwordChanged: 'true' } });
|
|
662
|
+
}
|
|
663
|
+
|
|
631
664
|
private buildAuthIndex() {
|
|
632
665
|
this.authIndex.clear();
|
|
633
666
|
if (!this.cache?.Permissions) return;
|
package/src/service/rest_url.ts
CHANGED
|
@@ -8,10 +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/
|
|
12
|
-
|
|
11
|
+
chpassURL: '/public/password/change', // keel revokes every refresh token on success
|
|
12
|
+
forgotPasswordURL: '/public/password/forgot',
|
|
13
|
+
confirmRegisterURL: '/public/register/confirm',
|
|
14
|
+
registerExchangeURL: '/public/register/exchange',
|
|
15
|
+
registerPartnerURL: '/api/register/partner',
|
|
13
16
|
plansURL: '/public/plans',
|
|
14
|
-
confirmChpassURL: '/public/
|
|
17
|
+
confirmChpassURL: '/public/password/reset',
|
|
15
18
|
twoFactorSetupURL: '/api/user/2fa/setup',
|
|
16
19
|
twoFactorVerifyURL: '/api/user/2fa/verify', // authenticated — confirms setup
|
|
17
20
|
twoFactorDisableURL: '/api/user/2fa/disable',
|