@ulabase/kit-ng 0.12.0
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 +238 -0
- package/fesm2022/ulabase-kit-ng.mjs +396 -0
- package/fesm2022/ulabase-kit-ng.mjs.map +1 -0
- package/package.json +34 -0
- package/types/ulabase-kit-ng.d.ts +326 -0
package/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# @ulabase/kit-ng
|
|
2
|
+
|
|
3
|
+
Wraps [`@ulabase/kit`](https://www.npmjs.com/package/@ulabase/kit) in Angular services with signals, route guards, and an HTTP interceptor — authentication in `RhAuthService`, and [payments](#rhpaymentsservice) in `RhPaymentsService`.
|
|
4
|
+
|
|
5
|
+
Pairs with [Ulabase](https://cloud.restheart.com), which gives you a production-ready backend — MongoDB, REST API, authentication, signup/signin, all managed.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ulabase/kit-ng @ulabase/kit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> **v0.3.0+ requires RESTHeart 9.6.0 or later.** Bearer-mode `activate()`, `resetPassword()`, and `switchTeam()` rely on the `delivery=body` query parameter, introduced in RESTHeart 9.6.0. Against an older server the request still succeeds, but the kit won't be able to capture the bearer token from the response — you'll need to log in again to get a token. Cookie mode is unaffected.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
In `app.config.ts`:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { provideRhAuth } from '@ulabase/kit-ng';
|
|
21
|
+
|
|
22
|
+
export const appConfig: ApplicationConfig = {
|
|
23
|
+
providers: [
|
|
24
|
+
provideRhAuth({ apiBaseUrl: environment.apiUrl }),
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That one call registers `RhAuthService`, adds the HTTP interceptor (attaches the Bearer token, clears session on 401), and sets up the DI config.
|
|
30
|
+
|
|
31
|
+
## How sessions work
|
|
32
|
+
|
|
33
|
+
The kit supports **two authentication modes**:
|
|
34
|
+
|
|
35
|
+
- **Bearer token** (default) — stored in `localStorage`, sent as `Authorization: Bearer <token>`
|
|
36
|
+
- **Cookie** — JWT managed by the backend as an HttpOnly cookie, **same-origin only**
|
|
37
|
+
|
|
38
|
+
Pass `mode: 'cookie'` to `login()`, `activate()`, `resetPassword()`, or `switchTeam()` only when the app is served from the same origin as the service. Since a Ulabase service lives on `*.restheart.com` while your app lives on your own domain, that cookie is third-party and is blocked by default in Safari and Firefox — regardless of the server's CORS configuration. **Cross-origin apps, which is the normal case, should stay on the default `'bearer'` mode.**
|
|
39
|
+
|
|
40
|
+
Each of these calls a matching auto-login endpoint with `delivery=body` (bearer) or `delivery=cookie`, and in bearer mode gets the fresh token back in the same response — no extra login round-trip.
|
|
41
|
+
|
|
42
|
+
For email verification, pass `delivery: 'fragment'` (default, cross-origin) or `delivery: 'cookie'` (same-origin) to `verify()`.
|
|
43
|
+
|
|
44
|
+
- `login()` stores the token in `localStorage` and schedules a proactive refresh at 80% of the token's TTL (~12 minutes for a 15-minute token).
|
|
45
|
+
- Every authenticated request sends `Authorization: Bearer <token>` automatically.
|
|
46
|
+
- Sessions survive page reloads as long as the token hasn't expired.
|
|
47
|
+
- If the token expires (laptop asleep, tab backgrounded too long, or the user hasn't interacted for 15+ minutes), the next API call gets a 401 and the session is cleared — the user sees "logged out," not a silent failure.
|
|
48
|
+
|
|
49
|
+
## `RhAuthService`
|
|
50
|
+
|
|
51
|
+
Inject it anywhere and use signals directly in templates:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
@Component({
|
|
55
|
+
template: `
|
|
56
|
+
@if (auth.isAuthenticated()) {
|
|
57
|
+
<span>{{ auth.user()?.profile?.firstName }}</span>
|
|
58
|
+
@if (auth.hasMultipleTeams()) {
|
|
59
|
+
<team-switcher [teams]="auth.teams()" />
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
})
|
|
64
|
+
export class AppComponent {
|
|
65
|
+
auth = inject(RhAuthService);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Signals
|
|
70
|
+
|
|
71
|
+
| Signal | Type | Description |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `user` | `Signal<UserInfo \| null>` | Authenticated user, or `null` |
|
|
74
|
+
| `isAuthenticated` | `Signal<boolean>` | Derived from `user` |
|
|
75
|
+
| `teams` | `Signal<TeamMembership[]>` | Teams the user belongs to |
|
|
76
|
+
| `hasMultipleTeams` | `Signal<boolean>` | `true` when user has more than one team |
|
|
77
|
+
|
|
78
|
+
### Methods
|
|
79
|
+
|
|
80
|
+
All methods return `Observable`:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
auth.checkSession() // Observable<UserInfo | null> — reads localStorage, no HTTP if no token
|
|
84
|
+
auth.login(email, password, mode?) // Observable<UserInfo> — mode: 'bearer' (default) | 'cookie'
|
|
85
|
+
auth.logout() // Observable<void>
|
|
86
|
+
auth.register(payload) // Observable<void>
|
|
87
|
+
auth.verify(email, token, delivery?) // Observable<string> — delivery: 'fragment' (default) | 'cookie'
|
|
88
|
+
auth.invite(email, role) // Observable<void>
|
|
89
|
+
auth.getInvitation(email, token) // Observable<Invitation>
|
|
90
|
+
auth.activate(payload, mode?) // Observable<void> — mode: 'bearer' (default) | 'cookie'
|
|
91
|
+
auth.acceptInvite(token) // Observable<void>
|
|
92
|
+
auth.switchTeam(teamId, mode?) // Observable<void> — mode: 'bearer' (default) | 'cookie'; re-fetches session
|
|
93
|
+
auth.forgotPassword(email) // Observable<void>
|
|
94
|
+
auth.resetPassword(payload, mode?) // Observable<void> — mode: 'bearer' (default) | 'cookie'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Your own collections
|
|
98
|
+
|
|
99
|
+
`provideRhAuth()` registers `rhAuthInterceptor`, which applies the session to every
|
|
100
|
+
`HttpClient` request bound for `apiBaseUrl`: the bearer token, the challenge suppression that
|
|
101
|
+
keeps the browser's Basic Auth popup away on a `401`, and the cookie credentials. So your own
|
|
102
|
+
data requests need no header of their own:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
private readonly http = inject(HttpClient);
|
|
106
|
+
|
|
107
|
+
load() {
|
|
108
|
+
return this.http.get(`${environment.apiUrl}/my-collection?pagesize=10`);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Requests to any other host pass through untouched — the token is a credential, and attaching
|
|
113
|
+
it everywhere would hand it to whatever third party the app happens to call. An
|
|
114
|
+
`Authorization` header you set yourself is left alone. The `401` handling applies to every
|
|
115
|
+
request either way, because that is about the session and not the target.
|
|
116
|
+
|
|
117
|
+
Adding your own interceptor alongside it takes a second `provideHttpClient` call, *after*
|
|
118
|
+
`provideRhAuth`; `withInterceptors` registers each function as a `multi` provider, so the two
|
|
119
|
+
add up rather than replacing one another. Do not name `rhAuthInterceptor` again there — it is
|
|
120
|
+
already registered, and repeating it just runs it twice.
|
|
121
|
+
|
|
122
|
+
### The kit's own calls go through `HttpClient` too
|
|
123
|
+
|
|
124
|
+
`provideRhAuth()` also hands the kit an `HttpClient`-backed transport, so a login, a session
|
|
125
|
+
check or a token renewal travels the same interceptor chain as everything else. Your tracing
|
|
126
|
+
header, retry policy or error handler covers all of it, not just the requests you wrote.
|
|
127
|
+
|
|
128
|
+
One asymmetry is deliberate: `rhAuthInterceptor` does **not** clear the session on a 401 to a
|
|
129
|
+
kit endpoint. A 401 there does not always mean the session is over —
|
|
130
|
+
`PATCH /auth/change-password` answers 401 for a wrong *current* password, `GET /token` for
|
|
131
|
+
wrong credentials — and signing a user out for mistyping their old password would be a poor
|
|
132
|
+
trade. The kit handles those itself; the interceptor's 401 handling is for your requests.
|
|
133
|
+
|
|
134
|
+
To route the kit somewhere else entirely, pass your own `transport` in the config: an explicit
|
|
135
|
+
one is left alone.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
provideRhAuth({ apiBaseUrl: environment.apiUrl, transport: myTransport }),
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Guards
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { authGuard, publicGuard } from '@ulabase/kit-ng';
|
|
145
|
+
|
|
146
|
+
export const routes: Routes = [
|
|
147
|
+
{
|
|
148
|
+
path: 'app',
|
|
149
|
+
canActivate: [authGuard], // redirects to /auth/login if not authenticated
|
|
150
|
+
loadComponent: () => import('./shell/shell.component'),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
path: 'auth/login',
|
|
154
|
+
canActivate: [publicGuard], // redirects to /app if already authenticated
|
|
155
|
+
loadComponent: () => import('./pages/login/login.component'),
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`authGuard` checks the in-memory token first — no HTTP call if the user is already authenticated in the current session.
|
|
161
|
+
|
|
162
|
+
## `RhPaymentsService`
|
|
163
|
+
|
|
164
|
+
Payments live in their own service, because a subscription is not a session. It needs the
|
|
165
|
+
`stripe` plugin on the service, and an explicit opt-in — without `payments: true` no
|
|
166
|
+
`/stripe/*` call is ever made:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
provideRhAuth({
|
|
170
|
+
apiBaseUrl: 'https://my-service.restheart.com',
|
|
171
|
+
payments: true,
|
|
172
|
+
ownershipRole: 'owner', // default; set it if your deployment overrides the role
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`subscription` loads on sign-in and reloads on `switchTeam` — nothing to wire up:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
@Component({
|
|
180
|
+
template: `
|
|
181
|
+
@if (payments.subscription(); as sub) {
|
|
182
|
+
<p>Plan: <strong>{{ sub.plan }}</strong> — {{ sub.active ? 'active' : 'inactive' }}</p>
|
|
183
|
+
@if (payments.canManageBilling()) {
|
|
184
|
+
<button (click)="upgrade()">Change plan</button>
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`,
|
|
188
|
+
})
|
|
189
|
+
export class BillingComponent {
|
|
190
|
+
payments = inject(RhPaymentsService);
|
|
191
|
+
|
|
192
|
+
upgrade() {
|
|
193
|
+
this.payments.createCheckoutSession('gold', 'month').subscribe({
|
|
194
|
+
next: ({ url }) => (window.location.href = url),
|
|
195
|
+
// 409 means "already subscribed" — Stripe's Portal handles plan changes
|
|
196
|
+
error: (err) => err.status === 409 && this.openPortal(),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
openPortal() {
|
|
201
|
+
this.payments.openBillingPortal().subscribe(({ url }) => (window.location.href = url));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Signals:** `subscription`, `plan`, `isSubscribed`, `canManageBilling`, `seatsAvailable`.
|
|
207
|
+
|
|
208
|
+
**Methods:** `loadSubscription`, `getPlans`, `createCheckoutSession`, `openBillingPortal`,
|
|
209
|
+
`getLicenses`, `grantLicense`, `revokeLicense`, `getCatalog`, `createOrder`, `getOrder`,
|
|
210
|
+
`waitForSubscription`, `waitForOrder` — each returning an `Observable`.
|
|
211
|
+
|
|
212
|
+
### The Checkout return page
|
|
213
|
+
|
|
214
|
+
The redirect back from Stripe races the webhook, so reading `subscription()` as the page
|
|
215
|
+
mounts can still show the old plan. Poll instead — and treat a timeout as "not yet", not as a
|
|
216
|
+
failed payment:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
this.payments.waitForSubscription(s => s.plan === 'gold' && s.active).subscribe({
|
|
220
|
+
next: () => this.status.set('success'),
|
|
221
|
+
error: (err) => this.status.set(err.name === 'WaitTimeoutError' ? 'pending' : 'error'),
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`waitForSubscription` updates the `subscription` signal itself when it resolves. See the
|
|
226
|
+
[core's payments guide](https://www.npmjs.com/package/@ulabase/kit#payments) for the
|
|
227
|
+
full reasoning.
|
|
228
|
+
|
|
229
|
+
## Quickstart
|
|
230
|
+
|
|
231
|
+
The fastest path to a working app:
|
|
232
|
+
|
|
233
|
+
1. Create a service on [Ulabase](https://cloud.restheart.com)
|
|
234
|
+
2. Fork [`restheart-cloud-starter-ng`](https://github.com/SoftInstigate/restheart-cloud-starter-ng)
|
|
235
|
+
3. Set `apiBaseUrl` in `environment.ts`
|
|
236
|
+
4. `ng serve`
|
|
237
|
+
|
|
238
|
+
You get login, signup, email verification, invitations, password reset, and multi-team switching — all wired up.
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { Injectable, InjectionToken, computed, effect, inject, makeEnvironmentProviders, signal, untracked } from "@angular/core";
|
|
3
|
+
import { catchError, firstValueFrom, from, map, of, switchMap, tap, throwError } from "rxjs";
|
|
4
|
+
import * as kit from "@ulabase/kit";
|
|
5
|
+
import { DEFAULT_CART_STORAGE_KEY, addToCart, cancelRefresh, cartTotals, clearToken, getToken, loadCart, removeFromCart, saveCart, setCartQuantity, toOrderItems } from "@ulabase/kit";
|
|
6
|
+
import { HttpClient, HttpContext, HttpContextToken, HttpErrorResponse, HttpHeaders, provideHttpClient, withInterceptors } from "@angular/common/http";
|
|
7
|
+
import { Router } from "@angular/router";
|
|
8
|
+
export * from "@ulabase/kit";
|
|
9
|
+
const RH_AUTH_CONFIG = new InjectionToken("RH_AUTH_CONFIG");
|
|
10
|
+
const RH_KIT_REQUEST = new HttpContextToken(() => false);
|
|
11
|
+
var RhAuthService = class RhAuthService {
|
|
12
|
+
config = inject(RH_AUTH_CONFIG);
|
|
13
|
+
_user = signal(null, ...ngDevMode ? [{ debugName: "_user" }] : /* istanbul ignore next */ []);
|
|
14
|
+
_teams = signal([], ...ngDevMode ? [{ debugName: "_teams" }] : /* istanbul ignore next */ []);
|
|
15
|
+
user = this._user.asReadonly();
|
|
16
|
+
teams = this._teams.asReadonly();
|
|
17
|
+
isAuthenticated = computed(() => this._user() !== null, ...ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ []);
|
|
18
|
+
hasMultipleTeams = computed(() => this._teams().length > 1, ...ngDevMode ? [{ debugName: "hasMultipleTeams" }] : /* istanbul ignore next */ []);
|
|
19
|
+
checkSession() {
|
|
20
|
+
if (!kit.getToken()) {
|
|
21
|
+
this._user.set(null);
|
|
22
|
+
this._teams.set([]);
|
|
23
|
+
return of(null);
|
|
24
|
+
}
|
|
25
|
+
return from(kit.checkSession(this.config)).pipe(tap((u) => {
|
|
26
|
+
this._user.set(u);
|
|
27
|
+
if (!u) this._teams.set([]);
|
|
28
|
+
}), switchMap((u) => u === null ? of([]) : from(kit.getTeams(this.config)).pipe(catchError(() => of([])))), tap((ts) => this._teams.set(ts)), map(() => this._user()));
|
|
29
|
+
}
|
|
30
|
+
register(payload) {
|
|
31
|
+
return from(kit.register(this.config, payload));
|
|
32
|
+
}
|
|
33
|
+
verify(email, token, delivery = "fragment") {
|
|
34
|
+
return from(kit.verify(this.config, email, token, delivery));
|
|
35
|
+
}
|
|
36
|
+
login(email, password, mode = "bearer") {
|
|
37
|
+
return from(kit.login(this.config, email, password, mode)).pipe(tap((u) => this._user.set(u)), switchMap((u) => from(kit.getTeams(this.config)).pipe(catchError(() => of([])), tap((ts) => this._teams.set(ts)), map(() => u))));
|
|
38
|
+
}
|
|
39
|
+
logout() {
|
|
40
|
+
return from(kit.logout(this.config)).pipe(tap(() => {
|
|
41
|
+
this._user.set(null);
|
|
42
|
+
this._teams.set([]);
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
invite(email, role) {
|
|
46
|
+
return from(kit.invite(this.config, email, role));
|
|
47
|
+
}
|
|
48
|
+
getInvitation(email, token) {
|
|
49
|
+
return from(kit.getInvitation(this.config, email, token));
|
|
50
|
+
}
|
|
51
|
+
activate(payload, mode = "bearer") {
|
|
52
|
+
return from(kit.activate(this.config, payload, mode)).pipe(map(() => void 0));
|
|
53
|
+
}
|
|
54
|
+
acceptInvite(token) {
|
|
55
|
+
return from(kit.acceptInvite(this.config, token)).pipe(switchMap(() => from(kit.getTeams(this.config)).pipe(catchError(() => of([])), tap((ts) => this._teams.set(ts)))), map(() => void 0));
|
|
56
|
+
}
|
|
57
|
+
resendInvite(email) {
|
|
58
|
+
return from(kit.resendInvite(this.config, email));
|
|
59
|
+
}
|
|
60
|
+
listInvitations() {
|
|
61
|
+
return from(kit.listInvitations(this.config));
|
|
62
|
+
}
|
|
63
|
+
loadTeams() {
|
|
64
|
+
return from(kit.getTeams(this.config)).pipe(tap((ts) => this._teams.set(ts)));
|
|
65
|
+
}
|
|
66
|
+
switchTeam(teamId, mode = "bearer") {
|
|
67
|
+
return from(kit.switchTeam(this.config, teamId, mode)).pipe(switchMap(() => this.checkSession()), map(() => void 0));
|
|
68
|
+
}
|
|
69
|
+
clearSession() {
|
|
70
|
+
kit.clearToken();
|
|
71
|
+
kit.cancelRefresh();
|
|
72
|
+
this._user.set(null);
|
|
73
|
+
this._teams.set([]);
|
|
74
|
+
}
|
|
75
|
+
forgotPassword(email) {
|
|
76
|
+
return from(kit.forgotPassword(this.config, email));
|
|
77
|
+
}
|
|
78
|
+
resetPassword(payload, mode = "bearer") {
|
|
79
|
+
return from(kit.resetPassword(this.config, payload, mode)).pipe(map(() => void 0));
|
|
80
|
+
}
|
|
81
|
+
listTeamMembers() {
|
|
82
|
+
return from(kit.listTeamMembers(this.config));
|
|
83
|
+
}
|
|
84
|
+
removeMember(email) {
|
|
85
|
+
return from(kit.removeMember(this.config, email));
|
|
86
|
+
}
|
|
87
|
+
updateMemberRole(email, role) {
|
|
88
|
+
return from(kit.updateMemberRole(this.config, email, role));
|
|
89
|
+
}
|
|
90
|
+
createTeam(teamName) {
|
|
91
|
+
return from(kit.createTeam(this.config, teamName));
|
|
92
|
+
}
|
|
93
|
+
updateTeam(updates) {
|
|
94
|
+
return from(kit.updateTeam(this.config, updates));
|
|
95
|
+
}
|
|
96
|
+
deleteTeam() {
|
|
97
|
+
return from(kit.deleteTeam(this.config));
|
|
98
|
+
}
|
|
99
|
+
updateProfile(updates) {
|
|
100
|
+
return from(kit.updateProfile(this.config, updates)).pipe(switchMap(() => this.checkSession()), map(() => void 0));
|
|
101
|
+
}
|
|
102
|
+
changePassword(currentPassword, newPassword) {
|
|
103
|
+
return from(kit.changePassword(this.config, currentPassword, newPassword));
|
|
104
|
+
}
|
|
105
|
+
updateUser(email, updates) {
|
|
106
|
+
return from(kit.updateUser(this.config, email, updates));
|
|
107
|
+
}
|
|
108
|
+
acceptConsents(body, mode = "bearer") {
|
|
109
|
+
const userId = this._user()?._id ?? kit.getTokenClaims()?.["sub"];
|
|
110
|
+
if (!userId) return throwError(() => ({
|
|
111
|
+
status: 0,
|
|
112
|
+
message: "acceptConsents requires a signed-in user"
|
|
113
|
+
}));
|
|
114
|
+
return from(kit.acceptConsents(this.config, userId, body, mode)).pipe(tap((u) => this._user.set(u)));
|
|
115
|
+
}
|
|
116
|
+
renewToken(mode = "bearer") {
|
|
117
|
+
return from(kit.renewToken(this.config, mode));
|
|
118
|
+
}
|
|
119
|
+
api(path, init) {
|
|
120
|
+
return from(kit.apiFetch(this.config, path, init));
|
|
121
|
+
}
|
|
122
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
123
|
+
minVersion: "12.0.0",
|
|
124
|
+
version: "22.2.0",
|
|
125
|
+
ngImport: i0,
|
|
126
|
+
type: RhAuthService,
|
|
127
|
+
deps: [],
|
|
128
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
129
|
+
});
|
|
130
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
131
|
+
minVersion: "12.0.0",
|
|
132
|
+
version: "22.2.0",
|
|
133
|
+
ngImport: i0,
|
|
134
|
+
type: RhAuthService,
|
|
135
|
+
providedIn: "root"
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
139
|
+
minVersion: "12.0.0",
|
|
140
|
+
version: "22.2.0",
|
|
141
|
+
ngImport: i0,
|
|
142
|
+
type: RhAuthService,
|
|
143
|
+
decorators: [{
|
|
144
|
+
type: Injectable,
|
|
145
|
+
args: [{ providedIn: "root" }]
|
|
146
|
+
}]
|
|
147
|
+
});
|
|
148
|
+
var RhPaymentsService = class RhPaymentsService {
|
|
149
|
+
config = inject(RH_AUTH_CONFIG);
|
|
150
|
+
auth = inject(RhAuthService);
|
|
151
|
+
_subscription = signal(null, ...ngDevMode ? [{ debugName: "_subscription" }] : /* istanbul ignore next */ []);
|
|
152
|
+
subscription = this._subscription.asReadonly();
|
|
153
|
+
plan = computed(() => this._subscription()?.plan ?? null, ...ngDevMode ? [{ debugName: "plan" }] : /* istanbul ignore next */ []);
|
|
154
|
+
isSubscribed = computed(() => this._subscription()?.active ?? false, ...ngDevMode ? [{ debugName: "isSubscribed" }] : /* istanbul ignore next */ []);
|
|
155
|
+
canManageBilling = computed(() => this.auth.user()?.team?.role === (this.config.ownershipRole ?? "owner"), ...ngDevMode ? [{ debugName: "canManageBilling" }] : /* istanbul ignore next */ []);
|
|
156
|
+
seatsAvailable = computed(() => this._subscription()?.seats?.available ?? null, ...ngDevMode ? [{ debugName: "seatsAvailable" }] : /* istanbul ignore next */ []);
|
|
157
|
+
teamKey = computed(() => {
|
|
158
|
+
const user = this.auth.user();
|
|
159
|
+
return user ? user.team?._id?.$oid ?? "" : null;
|
|
160
|
+
}, ...ngDevMode ? [{ debugName: "teamKey" }] : /* istanbul ignore next */ []);
|
|
161
|
+
get paymentsEnabled() {
|
|
162
|
+
return this.config.payments === true;
|
|
163
|
+
}
|
|
164
|
+
constructor() {
|
|
165
|
+
if (this.paymentsEnabled) effect(() => {
|
|
166
|
+
const teamKey = this.teamKey();
|
|
167
|
+
untracked(() => {
|
|
168
|
+
if (teamKey === null) this._subscription.set(null);
|
|
169
|
+
else this.loadSubscription().subscribe();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
loadSubscription() {
|
|
174
|
+
if (!this.paymentsEnabled) return of(null);
|
|
175
|
+
return from(kit.getSubscription(this.config)).pipe(tap((sub) => this._subscription.set(sub)), catchError(() => {
|
|
176
|
+
this._subscription.set(null);
|
|
177
|
+
return of(null);
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
getPlans() {
|
|
181
|
+
return from(kit.getPlans(this.config));
|
|
182
|
+
}
|
|
183
|
+
createCheckoutSession(plan, interval) {
|
|
184
|
+
return from(kit.createCheckoutSession(this.config, plan, interval));
|
|
185
|
+
}
|
|
186
|
+
openBillingPortal() {
|
|
187
|
+
return from(kit.openBillingPortal(this.config));
|
|
188
|
+
}
|
|
189
|
+
getLicenses() {
|
|
190
|
+
return from(kit.getLicenses(this.config));
|
|
191
|
+
}
|
|
192
|
+
grantLicense(userId) {
|
|
193
|
+
return from(kit.grantLicense(this.config, userId));
|
|
194
|
+
}
|
|
195
|
+
revokeLicense(userId) {
|
|
196
|
+
return from(kit.revokeLicense(this.config, userId));
|
|
197
|
+
}
|
|
198
|
+
getCatalog(opts) {
|
|
199
|
+
return from(kit.getCatalog(this.config, opts));
|
|
200
|
+
}
|
|
201
|
+
createOrder(items, email, collection) {
|
|
202
|
+
return from(kit.createOrder(this.config, items, email, collection));
|
|
203
|
+
}
|
|
204
|
+
getOrder(id, secret, collection) {
|
|
205
|
+
return from(kit.getOrder(this.config, id, secret, collection));
|
|
206
|
+
}
|
|
207
|
+
waitForSubscription(predicate, opts) {
|
|
208
|
+
return from(kit.waitForSubscription(this.config, predicate, opts).then((sub) => {
|
|
209
|
+
this._subscription.set(sub);
|
|
210
|
+
return sub;
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
waitForOrder(id, secret, opts) {
|
|
214
|
+
return from(kit.waitForOrder(this.config, id, secret, opts));
|
|
215
|
+
}
|
|
216
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
217
|
+
minVersion: "12.0.0",
|
|
218
|
+
version: "22.2.0",
|
|
219
|
+
ngImport: i0,
|
|
220
|
+
type: RhPaymentsService,
|
|
221
|
+
deps: [],
|
|
222
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
223
|
+
});
|
|
224
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
225
|
+
minVersion: "12.0.0",
|
|
226
|
+
version: "22.2.0",
|
|
227
|
+
ngImport: i0,
|
|
228
|
+
type: RhPaymentsService,
|
|
229
|
+
providedIn: "root"
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
233
|
+
minVersion: "12.0.0",
|
|
234
|
+
version: "22.2.0",
|
|
235
|
+
ngImport: i0,
|
|
236
|
+
type: RhPaymentsService,
|
|
237
|
+
decorators: [{
|
|
238
|
+
type: Injectable,
|
|
239
|
+
args: [{ providedIn: "root" }]
|
|
240
|
+
}],
|
|
241
|
+
ctorParameters: () => []
|
|
242
|
+
});
|
|
243
|
+
const RH_CART_STORAGE_KEY = new InjectionToken("RH_CART_STORAGE_KEY", {
|
|
244
|
+
providedIn: "root",
|
|
245
|
+
factory: () => DEFAULT_CART_STORAGE_KEY
|
|
246
|
+
});
|
|
247
|
+
var RhCartService = class RhCartService {
|
|
248
|
+
storageKey = inject(RH_CART_STORAGE_KEY);
|
|
249
|
+
_lines = signal(loadCart(this.storageKey), ...ngDevMode ? [{ debugName: "_lines" }] : /* istanbul ignore next */ []);
|
|
250
|
+
lines = this._lines.asReadonly();
|
|
251
|
+
totals = computed(() => cartTotals(this._lines()), ...ngDevMode ? [{ debugName: "totals" }] : /* istanbul ignore next */ []);
|
|
252
|
+
totalItems = computed(() => this.totals().totalItems, ...ngDevMode ? [{ debugName: "totalItems" }] : /* istanbul ignore next */ []);
|
|
253
|
+
subtotal = computed(() => this.totals().subtotal, ...ngDevMode ? [{ debugName: "subtotal" }] : /* istanbul ignore next */ []);
|
|
254
|
+
currency = computed(() => this.totals().currency, ...ngDevMode ? [{ debugName: "currency" }] : /* istanbul ignore next */ []);
|
|
255
|
+
orderItems = computed(() => toOrderItems(this._lines()), ...ngDevMode ? [{ debugName: "orderItems" }] : /* istanbul ignore next */ []);
|
|
256
|
+
add(item, quantity = 1) {
|
|
257
|
+
this.apply((current) => addToCart(current, item, quantity));
|
|
258
|
+
}
|
|
259
|
+
setQuantity(productId, quantity) {
|
|
260
|
+
this.apply((current) => setCartQuantity(current, productId, quantity));
|
|
261
|
+
}
|
|
262
|
+
remove(productId) {
|
|
263
|
+
this.apply((current) => removeFromCart(current, productId));
|
|
264
|
+
}
|
|
265
|
+
clear() {
|
|
266
|
+
this.apply(() => []);
|
|
267
|
+
}
|
|
268
|
+
apply(next) {
|
|
269
|
+
const updated = next(this._lines());
|
|
270
|
+
saveCart(updated, this.storageKey);
|
|
271
|
+
this._lines.set(updated);
|
|
272
|
+
}
|
|
273
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
274
|
+
minVersion: "12.0.0",
|
|
275
|
+
version: "22.2.0",
|
|
276
|
+
ngImport: i0,
|
|
277
|
+
type: RhCartService,
|
|
278
|
+
deps: [],
|
|
279
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
280
|
+
});
|
|
281
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
282
|
+
minVersion: "12.0.0",
|
|
283
|
+
version: "22.2.0",
|
|
284
|
+
ngImport: i0,
|
|
285
|
+
type: RhCartService,
|
|
286
|
+
providedIn: "root"
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
290
|
+
minVersion: "12.0.0",
|
|
291
|
+
version: "22.2.0",
|
|
292
|
+
ngImport: i0,
|
|
293
|
+
type: RhCartService,
|
|
294
|
+
decorators: [{
|
|
295
|
+
type: Injectable,
|
|
296
|
+
args: [{ providedIn: "root" }]
|
|
297
|
+
}]
|
|
298
|
+
});
|
|
299
|
+
const authGuard = () => {
|
|
300
|
+
const auth = inject(RhAuthService);
|
|
301
|
+
const router = inject(Router);
|
|
302
|
+
if (auth.isAuthenticated()) return true;
|
|
303
|
+
return auth.checkSession().pipe(map((user) => user !== null), tap((ok) => {
|
|
304
|
+
if (!ok) router.navigate(["/auth/login"]);
|
|
305
|
+
}));
|
|
306
|
+
};
|
|
307
|
+
const publicGuard = () => {
|
|
308
|
+
const auth = inject(RhAuthService);
|
|
309
|
+
const router = inject(Router);
|
|
310
|
+
if (!auth.isAuthenticated()) return true;
|
|
311
|
+
return auth.checkSession().pipe(map((user) => user === null), tap((isPublic) => {
|
|
312
|
+
if (!isPublic) router.navigate(["/"]);
|
|
313
|
+
}));
|
|
314
|
+
};
|
|
315
|
+
function readToken(config) {
|
|
316
|
+
return config.getToken ? config.getToken() : getToken();
|
|
317
|
+
}
|
|
318
|
+
function isPromise(v) {
|
|
319
|
+
return typeof v?.then === "function";
|
|
320
|
+
}
|
|
321
|
+
const rhAuthInterceptor = (req, next) => {
|
|
322
|
+
const auth = inject(RhAuthService);
|
|
323
|
+
const config = inject(RH_AUTH_CONFIG);
|
|
324
|
+
const ownsIts401s = req.context.get(RH_KIT_REQUEST);
|
|
325
|
+
const onError = (source) => source.pipe(catchError((err) => {
|
|
326
|
+
if (!ownsIts401s && err instanceof HttpErrorResponse && err.status === 401) {
|
|
327
|
+
clearToken();
|
|
328
|
+
cancelRefresh();
|
|
329
|
+
auth.clearSession();
|
|
330
|
+
}
|
|
331
|
+
return throwError(() => err);
|
|
332
|
+
}));
|
|
333
|
+
if (!req.url.startsWith(config.apiBaseUrl)) return next(req).pipe(onError);
|
|
334
|
+
const withCredentials = (token) => next(req.clone({
|
|
335
|
+
setHeaders: {
|
|
336
|
+
...token && !req.headers.has("Authorization") ? { Authorization: `Bearer ${token}` } : {},
|
|
337
|
+
...req.headers.has("No-Auth-Challenge") ? {} : { "No-Auth-Challenge": "true" }
|
|
338
|
+
},
|
|
339
|
+
withCredentials: true
|
|
340
|
+
})).pipe(onError);
|
|
341
|
+
const token = readToken(config);
|
|
342
|
+
return isPromise(token) ? from(token).pipe(switchMap(withCredentials)) : withCredentials(token);
|
|
343
|
+
};
|
|
344
|
+
const NULL_BODY_STATUS = /* @__PURE__ */ new Set([
|
|
345
|
+
101,
|
|
346
|
+
103,
|
|
347
|
+
204,
|
|
348
|
+
205,
|
|
349
|
+
304
|
|
350
|
+
]);
|
|
351
|
+
function toHttpHeaders(source) {
|
|
352
|
+
let headers = new HttpHeaders();
|
|
353
|
+
new Headers(source).forEach((value, name) => {
|
|
354
|
+
headers = headers.set(name, value);
|
|
355
|
+
});
|
|
356
|
+
return headers;
|
|
357
|
+
}
|
|
358
|
+
function toHeaders(source) {
|
|
359
|
+
const headers = new Headers();
|
|
360
|
+
for (const name of source.keys()) for (const value of source.getAll(name) ?? []) headers.append(name, value);
|
|
361
|
+
return headers;
|
|
362
|
+
}
|
|
363
|
+
function toResponse(body, status, statusText, headers) {
|
|
364
|
+
return new Response(NULL_BODY_STATUS.has(status) ? null : body, {
|
|
365
|
+
status,
|
|
366
|
+
statusText,
|
|
367
|
+
headers: toHeaders(headers)
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function httpClientTransport(http) {
|
|
371
|
+
return (url, init) => {
|
|
372
|
+
return firstValueFrom(http.request(init?.method ?? "GET", url, {
|
|
373
|
+
body: init?.body ?? null,
|
|
374
|
+
headers: toHttpHeaders(init?.headers),
|
|
375
|
+
observe: "response",
|
|
376
|
+
responseType: "text",
|
|
377
|
+
context: new HttpContext().set(RH_KIT_REQUEST, true),
|
|
378
|
+
withCredentials: true
|
|
379
|
+
})).then((res) => toResponse(res.body, res.status, res.statusText, res.headers), (err) => {
|
|
380
|
+
if (err instanceof HttpErrorResponse && err.status !== 0) return toResponse(typeof err.error === "string" ? err.error : JSON.stringify(err.error ?? null), err.status, err.statusText, err.headers);
|
|
381
|
+
throw err;
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function provideRhAuth(config) {
|
|
386
|
+
return makeEnvironmentProviders([provideHttpClient(withInterceptors([rhAuthInterceptor])), {
|
|
387
|
+
provide: RH_AUTH_CONFIG,
|
|
388
|
+
useFactory: () => ({
|
|
389
|
+
...config,
|
|
390
|
+
transport: config.transport ?? httpClientTransport(inject(HttpClient))
|
|
391
|
+
})
|
|
392
|
+
}]);
|
|
393
|
+
}
|
|
394
|
+
export { RH_AUTH_CONFIG, RH_CART_STORAGE_KEY, RhAuthService, RhCartService, RhPaymentsService, authGuard, httpClientTransport, provideRhAuth, publicGuard, rhAuthInterceptor };
|
|
395
|
+
|
|
396
|
+
//# sourceMappingURL=ulabase-kit-ng.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ulabase-kit-ng.mjs","names":[],"sources":["../../src/tokens.ts","../../src/auth.service.ts","../../src/payments.service.ts","../../src/cart.service.ts","../../src/auth.guard.ts","../../src/auth.interceptor.ts","../../src/http-transport.ts","../../src/provide-rh-auth.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { HttpContextToken } from '@angular/common/http';\nimport type { AuthConfig } from '@ulabase/kit';\n\nexport const RH_AUTH_CONFIG = new InjectionToken<AuthConfig>('RH_AUTH_CONFIG');\n\n\n/**\n * Marks a request as originating from the kit rather than from application\n * code. Set by `httpClientTransport`, read by `rhAuthInterceptor`.\n *\n * The kit owns the meaning of a 401 on its own endpoints, and that meaning is\n * not always \"the session is dead\": `PATCH /auth/change-password` answers 401\n * when the *current password* is wrong, and `GET /token` when the credentials\n * are. Letting the interceptor clear the session on those would sign a user\n * out for mistyping their old password.\n */\nexport const RH_KIT_REQUEST = new HttpContextToken<boolean>(() => false);\n","import { Injectable, computed, inject, signal } from '@angular/core';\nimport { Observable, catchError, from, map, of, switchMap, tap, throwError } from 'rxjs';\nimport type { UserInfo, TeamMembership, TeamMember, PendingInvitation, AuthConfig, LoginMode } from '@ulabase/kit';\nimport * as kit from '@ulabase/kit';\nimport { RH_AUTH_CONFIG } from './tokens.js';\n\n@Injectable({ providedIn: 'root' })\nexport class RhAuthService {\n private readonly config: AuthConfig = inject(RH_AUTH_CONFIG);\n\n private readonly _user = signal<UserInfo | null>(null);\n private readonly _teams = signal<TeamMembership[]>([]);\n\n readonly user = this._user.asReadonly();\n readonly teams = this._teams.asReadonly();\n readonly isAuthenticated = computed(() => this._user() !== null);\n readonly hasMultipleTeams = computed(() => this._teams().length > 1);\n\n /**\n * Check the current session state.\n *\n * Reads the token from localStorage — if present and not expired,\n * returns user info from the server. Otherwise returns null.\n */\n checkSession(): Observable<UserInfo | null> {\n // If no valid token in localStorage, we're logged out — no HTTP call needed\n if (!kit.getToken()) {\n this._user.set(null);\n this._teams.set([]);\n return of(null);\n }\n\n return from(kit.checkSession(this.config)).pipe(\n tap(u => {\n this._user.set(u);\n if (!u) {\n this._teams.set([]);\n }\n }),\n switchMap(u =>\n u === null\n ? of([])\n : from(kit.getTeams(this.config)).pipe(catchError(() => of([])))\n ),\n tap(ts => this._teams.set(ts)),\n map(() => this._user())\n );\n }\n\n register(payload: {\n email: string;\n password: string;\n teamName: string;\n firstName: string;\n lastName: string;\n [key: string]: unknown;\n }): Observable<void> {\n return from(kit.register(this.config, payload));\n }\n\n verify(email: string, token: string, delivery: 'cookie' | 'fragment' = 'fragment'): Observable<string> {\n return from(kit.verify(this.config, email, token, delivery));\n }\n\n login(email: string, password: string, mode: LoginMode = 'bearer'): Observable<UserInfo> {\n return from(kit.login(this.config, email, password, mode)).pipe(\n tap(u => this._user.set(u)),\n switchMap(u =>\n from(kit.getTeams(this.config)).pipe(\n catchError(() => of([])),\n tap(ts => this._teams.set(ts)),\n map(() => u)\n )\n )\n );\n }\n\n logout(): Observable<void> {\n return from(kit.logout(this.config)).pipe(\n tap(() => {\n this._user.set(null);\n this._teams.set([]);\n })\n );\n }\n\n invite(email: string, role: 'owner' | 'member'): Observable<void> {\n return from(kit.invite(this.config, email, role));\n }\n\n getInvitation(email: string, token: string) {\n return from(kit.getInvitation(this.config, email, token));\n }\n\n activate(payload: { email: string; token: string; password: string }, mode: LoginMode = 'bearer'): Observable<void> {\n return from(kit.activate(this.config, payload, mode)).pipe(map(() => undefined));\n }\n\n acceptInvite(token: string): Observable<void> {\n return from(kit.acceptInvite(this.config, token)).pipe(\n switchMap(() =>\n from(kit.getTeams(this.config)).pipe(\n catchError(() => of([])),\n tap(ts => this._teams.set(ts))\n )\n ),\n map(() => undefined)\n );\n }\n\n resendInvite(email: string): Observable<void> {\n return from(kit.resendInvite(this.config, email));\n }\n\n listInvitations(): Observable<PendingInvitation[]> {\n return from(kit.listInvitations(this.config));\n }\n\n loadTeams(): Observable<TeamMembership[]> {\n return from(kit.getTeams(this.config)).pipe(\n tap(ts => this._teams.set(ts))\n );\n }\n\n switchTeam(teamId: { $oid: string }, mode: LoginMode = 'bearer'): Observable<void> {\n return from(kit.switchTeam(this.config, teamId, mode)).pipe(\n switchMap(() => this.checkSession()),\n map(() => undefined)\n );\n }\n\n clearSession(): void {\n kit.clearToken();\n kit.cancelRefresh();\n this._user.set(null);\n this._teams.set([]);\n }\n\n forgotPassword(email: string): Observable<void> {\n return from(kit.forgotPassword(this.config, email));\n }\n\n resetPassword(payload: { email: string; token: string; password: string }, mode: LoginMode = 'bearer'): Observable<void> {\n return from(kit.resetPassword(this.config, payload, mode)).pipe(map(() => undefined));\n }\n\n // ── Team members ─────────────────────────────────────────────────────────\n\n listTeamMembers(): Observable<TeamMember[]> {\n return from(kit.listTeamMembers(this.config));\n }\n\n removeMember(email: string): Observable<void> {\n return from(kit.removeMember(this.config, email));\n }\n\n updateMemberRole(email: string, role: 'owner' | 'member'): Observable<void> {\n return from(kit.updateMemberRole(this.config, email, role));\n }\n\n // ── Team lifecycle ───────────────────────────────────────────────────────\n\n createTeam(teamName: string): Observable<TeamMembership> {\n return from(kit.createTeam(this.config, teamName));\n }\n\n updateTeam(updates: { name?: string; description?: string }): Observable<void> {\n return from(kit.updateTeam(this.config, updates));\n }\n\n deleteTeam(): Observable<void> {\n return from(kit.deleteTeam(this.config));\n }\n\n // ── Profile & password ──────────────────────────────────────────────────\n\n updateProfile(updates: { firstName?: string; lastName?: string }): Observable<void> {\n return from(kit.updateProfile(this.config, updates)).pipe(\n switchMap(() => this.checkSession()),\n map(() => undefined)\n );\n }\n\n changePassword(currentPassword: string, newPassword: string): Observable<void> {\n return from(kit.changePassword(this.config, currentPassword, newPassword));\n }\n\n updateUser(email: string, updates: Record<string, unknown>): Observable<void> {\n return from(kit.updateUser(this.config, email, updates));\n }\n\n // ── Consents ────────────────────────────────────────────────────────────\n\n /**\n * Record the signed-in user's acceptance of the application's consents,\n * renew the token so the guard sees it, and update the `user` signal.\n */\n acceptConsents(body?: Record<string, unknown>, mode: LoginMode = 'bearer'): Observable<UserInfo> {\n // The user document may be missing precisely because the rule is blocking\n // `/users/me` — the case this call exists to get out of. The token still\n // says who they are.\n const userId = this._user()?._id ?? (kit.getTokenClaims()?.['sub'] as string | undefined);\n if (!userId) {\n return throwError(() => ({ status: 0, message: 'acceptConsents requires a signed-in user' }));\n }\n return from(kit.acceptConsents(this.config, userId, body, mode)).pipe(\n tap(u => this._user.set(u))\n );\n }\n\n /** Force a new token, carrying the user document as it is now. */\n renewToken(mode: LoginMode = 'bearer'): Observable<string | null> {\n return from(kit.renewToken(this.config, mode));\n }\n\n /**\n * An authenticated `fetch` against the service, returned as an Observable.\n *\n * The Angular counterpart of React's `auth.api()` — the bearer token is\n * attached automatically, and the request goes through the Angular\n * interceptor chain (when `httpClientTransport` is configured).\n *\n * ```ts\n * this.auth.api('/demo').pipe(\n * switchMap(res => res.json()),\n * ).subscribe(data => console.log(data));\n * ```\n *\n * For a POST:\n * ```ts\n * this.auth.api('/demo', {\n * method: 'POST',\n * body: JSON.stringify({ name: 'hello' }),\n * }).pipe(switchMap(res => res.json()));\n * ```\n *\n * Rejects with an `ApiError` (`{ status, message }`) on any non-2xx response.\n */\n api(path: string, init?: RequestInit): Observable<Response> {\n return from(kit.apiFetch(this.config, path, init));\n }\n}\n","import { Injectable, computed, effect, inject, signal, untracked } from '@angular/core';\nimport { Observable, catchError, from, of, tap } from 'rxjs';\nimport type {\n AuthConfig,\n CatalogItem,\n GrantLicenseResult,\n Licenses,\n Order,\n Plan,\n Subscription,\n WaitOptions,\n CatalogQuery,\n} from '@ulabase/kit';\nimport * as kit from '@ulabase/kit';\nimport { RH_AUTH_CONFIG } from './tokens.js';\nimport { RhAuthService } from './auth.service.js';\n\n/**\n * Subscription, billing and order management.\n *\n * Separated from {@link RhAuthService} because payments are not authentication.\n * Reads the current user from {@link RhAuthService} to derive\n * `canManageBilling`, but owns its own subscription state.\n *\n * Subscription is loaded automatically when the user signs in and reloaded\n * when they switch team (via an `effect` watching {@link teamKey}). No manual\n * wiring needed.\n *\n * Only active when `config.payments` is `true` — otherwise every method\n * is a no-op and no `/stripe/*` call is ever made.\n */\n@Injectable({ providedIn: 'root' })\nexport class RhPaymentsService {\n private readonly config: AuthConfig = inject(RH_AUTH_CONFIG);\n private readonly auth = inject(RhAuthService);\n\n private readonly _subscription = signal<Subscription | null>(null);\n\n readonly subscription = this._subscription.asReadonly();\n readonly plan = computed(() => this._subscription()?.plan ?? null);\n readonly isSubscribed = computed(() => this._subscription()?.active ?? false);\n readonly canManageBilling = computed(\n () => this.auth.user()?.team?.role === (this.config.ownershipRole ?? 'owner')\n );\n readonly seatsAvailable = computed(() => this._subscription()?.seats?.available ?? null);\n\n /**\n * The team the subscription belongs to: `null` when signed out, `''` for a\n * signed-in user with no team.\n *\n * The effect below tracks this rather than `auth.user()` itself. Both\n * `updateProfile` and `acceptConsents` re-run `checkSession`, which replaces\n * the user with a freshly parsed document — tracking that object's identity\n * would reload the subscription on every profile edit and consent\n * acceptance, which the adapter contract rules out. A subscription changes\n * with the team, not with the profile.\n */\n private readonly teamKey = computed(() => {\n const user = this.auth.user();\n return user ? (user.team?._id?.$oid ?? '') : null;\n });\n\n private get paymentsEnabled(): boolean {\n return this.config.payments === true;\n }\n\n constructor() {\n if (this.paymentsEnabled) {\n effect(() => {\n const teamKey = this.teamKey();\n untracked(() => {\n if (teamKey === null) {\n this._subscription.set(null);\n } else {\n this.loadSubscription().subscribe();\n }\n });\n });\n }\n }\n\n /** Reload the team's subscription. No-op when `config.payments` is not `true`. */\n loadSubscription(): Observable<Subscription | null> {\n if (!this.paymentsEnabled) return of(null);\n return from(kit.getSubscription(this.config)).pipe(\n tap(sub => this._subscription.set(sub)),\n catchError(() => {\n this._subscription.set(null);\n return of(null);\n })\n );\n }\n\n /** The service's subscription plan catalog. No session required. */\n getPlans(): Observable<{ default_plan: string; plans: Plan[] }> {\n return from(kit.getPlans(this.config));\n }\n\n /**\n * Start a Stripe Checkout session.\n *\n * **Rejects with `status: 409`** when the team already has an active\n * subscription — send them to {@link openBillingPortal} instead.\n */\n createCheckoutSession(plan: string, interval: 'month' | 'year'): Observable<{ url: string }> {\n return from(kit.createCheckoutSession(this.config, plan, interval));\n }\n\n /** Open the Stripe Customer Portal for self-service plan changes. */\n openBillingPortal(): Observable<{ url: string }> {\n return from(kit.openBillingPortal(this.config));\n }\n\n /** The team's seat licences. Requires `canManageBilling`. */\n getLicenses(): Observable<Licenses> {\n return from(kit.getLicenses(this.config));\n }\n\n /**\n * Grant a seat licence. Returns `'granted'` or `'already-licensed'`.\n * Rejects with `status: 409` when no seat is available.\n */\n grantLicense(userId: string): Observable<GrantLicenseResult> {\n return from(kit.grantLicense(this.config, userId));\n }\n\n /** Revoke a seat licence. */\n revokeLicense(userId: string): Observable<void> {\n return from(kit.revokeLicense(this.config, userId));\n }\n\n /** Read the product catalog. */\n getCatalog(opts?: CatalogQuery): Observable<CatalogItem[]> {\n return from(kit.getCatalog(this.config, opts));\n }\n\n /**\n * Create an order and start Checkout.\n *\n * @param email Required for guest checkout.\n */\n createOrder(\n items: { productId: string; quantity: number }[],\n email?: string,\n collection?: string\n ): Observable<{ _id: { $oid: string }; checkout_url: string; secret: string }> {\n return from(kit.createOrder(this.config, items, email, collection));\n }\n\n /** Read an order back. */\n getOrder(id: string, secret?: string, collection?: string): Observable<Order> {\n return from(kit.getOrder(this.config, id, secret, collection));\n }\n\n /**\n * Poll until the subscription satisfies `predicate`.\n *\n * Use on the Checkout success page — the redirect back from Stripe races\n * the webhook, so a bare `getSubscription` can still show the old plan.\n *\n * **Timing out is not a payment failure.** Rejects with\n * `WaitTimeoutError` when the webhook hasn't arrived yet.\n */\n waitForSubscription(\n predicate: (subscription: Subscription) => boolean,\n opts?: WaitOptions\n ): Observable<Subscription> {\n return from(\n kit.waitForSubscription(this.config, predicate, opts).then(sub => {\n this._subscription.set(sub);\n return sub;\n })\n );\n }\n\n /**\n * Poll until the order leaves `'pending_payment'`.\n *\n * Same race as {@link waitForSubscription} — use on the order success page.\n *\n * **Timing out is not a payment failure.** Rejects with\n * `WaitTimeoutError` when the webhook hasn't arrived yet.\n */\n waitForOrder(\n id: string,\n secret?: string,\n opts?: WaitOptions & { collection?: string }\n ): Observable<Order> {\n return from(kit.waitForOrder(this.config, id, secret, opts));\n }\n}\n","import { Injectable, InjectionToken, computed, inject, signal } from '@angular/core';\nimport {\n addToCart,\n cartTotals,\n loadCart,\n removeFromCart,\n saveCart,\n setCartQuantity,\n toOrderItems,\n DEFAULT_CART_STORAGE_KEY,\n type CartItem,\n type CartLine,\n} from '@ulabase/kit';\n/**\n * Where {@link RhCartService} keeps the cart in `localStorage`. Defaults to `'rh-cart'`.\n *\n * Worth providing when two of your apps share an origin, which is one deployment decision away\n * from happening by accident.\n *\n * Declared here rather than in `tokens.ts`: that file is the authentication surface, and a cart\n * needs no session. Importing the kit into it to reach one constant also dragged the kit's module\n * into every spec that touches `RH_AUTH_CONFIG`, where it is auto-mocked — which shifted module\n * initialisation enough to make two unrelated guard tests fail, in CI only.\n */\nexport const RH_CART_STORAGE_KEY = new InjectionToken<string>('RH_CART_STORAGE_KEY', {\n providedIn: 'root',\n factory: () => DEFAULT_CART_STORAGE_KEY,\n});\n\n/**\n * The shopping cart.\n *\n * Independent of {@link import('./auth.service.js').RhAuthService | RhAuthService}\n * and of `RH_AUTH_CONFIG`: a cart belongs to the browser, not to a session, and\n * a shop that makes people sign in before they can put something in a basket\n * loses most of them there. It becomes an order — which does need a config —\n * when {@link RhCartService.orderItems} is handed to `createOrder`.\n *\n * On the server, `localStorage` does not exist and the cart reads as empty.\n * That is correct for a rendered page — a request carries no basket — but it\n * does mean a server-rendered cart count starts at zero and fills in once the\n * browser takes over.\n */\n@Injectable({ providedIn: 'root' })\nexport class RhCartService {\n private readonly storageKey = inject(RH_CART_STORAGE_KEY);\n\n // Declared after `storageKey` so the field is there: initializers run in order.\n private readonly _lines = signal<CartLine[]>(loadCart(this.storageKey));\n\n /** The lines, in the order they were added. */\n readonly lines = this._lines.asReadonly();\n\n private readonly totals = computed(() => cartTotals(this._lines()));\n\n /** Units, not lines: two of one thing counts two. */\n readonly totalItems = computed(() => this.totals().totalItems);\n /** Display only. Minor units, meaningful when every line shares a currency. */\n readonly subtotal = computed(() => this.totals().subtotal);\n /** The first line's currency, or `'eur'` when empty. */\n readonly currency = computed(() => this.totals().currency);\n /** The cart as `createOrder` wants it. */\n readonly orderItems = computed(() => toOrderItems(this._lines()));\n\n /** Adds an item, or increases the line already holding it. */\n add(item: CartItem, quantity = 1): void {\n this.apply(current => addToCart(current, item, quantity));\n }\n\n /** Sets a line's quantity. Zero removes it. */\n setQuantity(productId: string, quantity: number): void {\n this.apply(current => setCartQuantity(current, productId, quantity));\n }\n\n remove(productId: string): void {\n this.apply(current => removeFromCart(current, productId));\n }\n\n clear(): void {\n this.apply(() => []);\n }\n\n /**\n * State and storage move together, in the operation that changed them.\n *\n * Deliberately not an `effect` reacting to the signal: an effect runs after\n * the fact and can be skipped or reordered, and the failure that produces is\n * a line coming back from storage after somebody removed it.\n */\n private apply(next: (current: CartLine[]) => CartLine[]): void {\n const updated = next(this._lines());\n saveCart(updated, this.storageKey);\n this._lines.set(updated);\n }\n}\n","import { inject } from '@angular/core';\nimport { Router, type CanActivateFn } from '@angular/router';\nimport { map, tap } from 'rxjs';\nimport { RhAuthService } from './auth.service.js';\n\nexport const authGuard: CanActivateFn = () => {\n const auth = inject(RhAuthService);\n const router = inject(Router);\n\n if (auth.isAuthenticated()) return true;\n\n return auth.checkSession().pipe(\n map(user => user !== null),\n tap(ok => { if (!ok) router.navigate(['/auth/login']); })\n );\n};\n\nexport const publicGuard: CanActivateFn = () => {\n const auth = inject(RhAuthService);\n const router = inject(Router);\n\n if (!auth.isAuthenticated()) return true;\n\n return auth.checkSession().pipe(\n map(user => user === null),\n tap(isPublic => { if (!isPublic) router.navigate(['/']); })\n );\n};\n","import { inject } from '@angular/core';\nimport {\n HttpRequest,\n HttpHandlerFn,\n HttpInterceptorFn,\n HttpErrorResponse,\n} from '@angular/common/http';\nimport { Observable, catchError, from, switchMap, throwError } from 'rxjs';\nimport { RhAuthService } from './auth.service.js';\nimport { RH_AUTH_CONFIG, RH_KIT_REQUEST } from './tokens.js';\nimport { clearToken, cancelRefresh, getToken } from '@ulabase/kit';\nimport type { AuthConfig } from '@ulabase/kit';\n\n/** The token source is pluggable and may be async — keep the sync path sync. */\nfunction readToken(config: AuthConfig): string | null | Promise<string | null> {\n return config.getToken ? config.getToken() : getToken();\n}\n\nfunction isPromise<T>(v: T | Promise<T>): v is Promise<T> {\n return typeof (v as Promise<T>)?.then === 'function';\n}\n\n/**\n * Authenticates the application's own `HttpClient` requests, and clears the\n * session when the service rejects one with a 401.\n *\n * The kit's own calls go out through `fetch` and never reach an Angular\n * interceptor, so without this an app querying its own collections had to\n * attach the bearer token by hand at every call site.\n *\n * **Only requests to `apiBaseUrl` are touched.** The token is a credential:\n * attaching it to every outgoing request would hand it to any third-party host\n * the app happens to call. Requests elsewhere pass through untouched — they\n * still get the 401 handling, which is about the session and not the target.\n *\n * An `Authorization` header already on the request is left alone, so a call\n * that needs different credentials can say so.\n */\nexport const rhAuthInterceptor: HttpInterceptorFn = (\n req: HttpRequest<unknown>,\n next: HttpHandlerFn\n) => {\n const auth = inject(RhAuthService);\n const config = inject(RH_AUTH_CONFIG);\n\n // A 401 on the kit's own endpoints does not always mean the session is dead\n // — change-password and token answer 401 for wrong *supplied* credentials —\n // and the kit already handles those. Clearing here would sign a user out for\n // mistyping their old password.\n const ownsIts401s = req.context.get(RH_KIT_REQUEST);\n\n const onError = (source: Observable<unknown>) =>\n source.pipe(\n catchError((err: unknown) => {\n if (!ownsIts401s && err instanceof HttpErrorResponse && err.status === 401) {\n clearToken();\n cancelRefresh();\n auth.clearSession();\n }\n return throwError(() => err);\n })\n );\n\n if (!req.url.startsWith(config.apiBaseUrl)) {\n return next(req).pipe(onError) as ReturnType<HttpHandlerFn>;\n }\n\n const withCredentials = (token: string | null) =>\n next(\n req.clone({\n setHeaders: {\n ...(token && !req.headers.has('Authorization')\n ? { Authorization: `Bearer ${token}` }\n : {}),\n // Suppress RESTHeart's WWW-Authenticate challenge on a 401 —\n // without it the browser shows its native Basic Auth popup.\n ...(req.headers.has('No-Auth-Challenge') ? {} : { 'No-Auth-Challenge': 'true' }),\n },\n // Matches the kit's own `credentials: 'include'`, so a cookie-mode\n // session authenticates these requests too.\n withCredentials: true,\n })\n ).pipe(onError) as ReturnType<HttpHandlerFn>;\n\n const token = readToken(config);\n return isPromise(token)\n ? (from(token).pipe(switchMap(withCredentials)) as ReturnType<HttpHandlerFn>)\n : withCredentials(token);\n};\n","import {\n HttpClient,\n HttpContext,\n HttpErrorResponse,\n HttpHeaders,\n HttpResponse,\n} from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport type { AuthConfig } from '@ulabase/kit';\nimport { RH_KIT_REQUEST } from './tokens.js';\n\n/** Statuses the Fetch spec forbids a body on — `new Response(body, …)` throws. */\nconst NULL_BODY_STATUS = new Set([101, 103, 204, 205, 304]);\n\nfunction toHttpHeaders(source: HeadersInit | undefined): HttpHeaders {\n let headers = new HttpHeaders();\n new Headers(source).forEach((value, name) => {\n headers = headers.set(name, value);\n });\n return headers;\n}\n\nfunction toHeaders(source: HttpHeaders): Headers {\n const headers = new Headers();\n for (const name of source.keys()) {\n for (const value of source.getAll(name) ?? []) {\n headers.append(name, value);\n }\n }\n return headers;\n}\n\nfunction toResponse(body: string | null, status: number, statusText: string, headers: HttpHeaders): Response {\n return new Response(NULL_BODY_STATUS.has(status) ? null : body, {\n status,\n statusText,\n headers: toHeaders(headers),\n });\n}\n\n/**\n * A {@link AuthConfig.transport} backed by Angular's `HttpClient`, so the kit's\n * own calls go through the interceptor chain like everything else the\n * application sends.\n *\n * Without it the kit speaks `fetch` directly and an interceptor never sees a\n * login, a session check or a token renewal — which makes any cross-cutting\n * concern written as an interceptor quietly partial.\n *\n * Two mismatches with `fetch` are reconciled here:\n *\n * - **`HttpClient` throws on a non-2xx response.** `fetch` resolves and lets\n * the caller read the status, which is what the core does to build its\n * `ApiError`. So an `HttpErrorResponse` carrying a real response is turned\n * back into a resolved `Response`.\n * - **`status === 0` is not a response**, it is a network or CORS failure with\n * nothing behind it. `new Response` would reject the status anyway, so the\n * original error is rethrown and surfaces as a rejected promise, exactly as\n * a failed `fetch` would.\n *\n * The body is read as text and left unparsed: the core calls `.json()` itself,\n * and on an error response it needs the raw body to pull out the message.\n */\nexport function httpClientTransport(http: HttpClient) {\n return (url: string, init?: RequestInit): Promise<Response> => {\n const request = firstValueFrom(\n http.request(init?.method ?? 'GET', url, {\n body: init?.body ?? null,\n headers: toHttpHeaders(init?.headers as HeadersInit | undefined),\n observe: 'response',\n responseType: 'text',\n // The kit handles 401s on its own endpoints; see RH_KIT_REQUEST.\n context: new HttpContext().set(RH_KIT_REQUEST, true),\n // Mirrors the core's `credentials: 'include'` — cookie-mode sessions\n // authenticate on the cookie alone.\n withCredentials: true,\n })\n );\n\n return request.then(\n (res: HttpResponse<string>) => toResponse(res.body, res.status, res.statusText, res.headers),\n (err: unknown) => {\n if (err instanceof HttpErrorResponse && err.status !== 0) {\n const body = typeof err.error === 'string' ? err.error : JSON.stringify(err.error ?? null);\n return toResponse(body, err.status, err.statusText, err.headers);\n }\n throw err;\n }\n );\n };\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';\nimport type { AuthConfig } from '@ulabase/kit';\nimport { RH_AUTH_CONFIG } from './tokens.js';\nimport { rhAuthInterceptor } from './auth.interceptor.js';\nimport { httpClientTransport } from './http-transport.js';\n\nexport function provideRhAuth(config: AuthConfig): EnvironmentProviders {\n return makeEnvironmentProviders([\n provideHttpClient(withInterceptors([rhAuthInterceptor])),\n {\n provide: RH_AUTH_CONFIG,\n // A factory, not a value, so the kit's own calls can be routed through\n // `HttpClient` — which does not exist yet when the caller builds the\n // config. An explicit `transport` is left alone, so an application can\n // still supply its own.\n useFactory: (): AuthConfig => ({\n ...config,\n transport: config.transport ?? httpClientTransport(inject(HttpClient)),\n }),\n },\n ]);\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ulabase/kit-ng",
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "Angular services, route guards, and HTTP interceptor for Ulabase authentication and payments — wraps @ulabase/kit.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/ulabase/kit.git",
|
|
8
|
+
"directory": "packages/kit-ng"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"@angular/common": ">=21",
|
|
13
|
+
"@angular/core": ">=21",
|
|
14
|
+
"@angular/router": ">=21",
|
|
15
|
+
"rxjs": ">=7"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@ulabase/kit": "0.12.0",
|
|
19
|
+
"tslib": "^2.3.0"
|
|
20
|
+
},
|
|
21
|
+
"module": "fesm2022/ulabase-kit-ng.mjs",
|
|
22
|
+
"typings": "types/ulabase-kit-ng.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
"./package.json": {
|
|
25
|
+
"default": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./types/ulabase-kit-ng.d.ts",
|
|
29
|
+
"default": "./fesm2022/ulabase-kit-ng.mjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"type": "module"
|
|
34
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { Observable } from "rxjs";
|
|
2
|
+
import * as kit from "@ulabase/kit";
|
|
3
|
+
import { AuthConfig, CartItem, CartLine, CatalogItem, CatalogQuery, GrantLicenseResult, Licenses, LoginMode, Order, PendingInvitation, Plan, Subscription, TeamMember, TeamMembership, UserInfo, WaitOptions } from "@ulabase/kit";
|
|
4
|
+
import * as i0 from "@angular/core";
|
|
5
|
+
import { EnvironmentProviders, InjectionToken } from "@angular/core";
|
|
6
|
+
import { CanActivateFn } from "@angular/router";
|
|
7
|
+
import { HttpClient, HttpContextToken, HttpInterceptorFn } from "@angular/common/http";
|
|
8
|
+
export * from "@ulabase/kit";
|
|
9
|
+
export declare class RhAuthService {
|
|
10
|
+
private readonly config;
|
|
11
|
+
private readonly _user;
|
|
12
|
+
private readonly _teams;
|
|
13
|
+
readonly user: import("@angular/core").Signal<UserInfo | null>;
|
|
14
|
+
readonly teams: import("@angular/core").Signal<TeamMembership[]>;
|
|
15
|
+
readonly isAuthenticated: import("@angular/core").Signal<boolean>;
|
|
16
|
+
readonly hasMultipleTeams: import("@angular/core").Signal<boolean>;
|
|
17
|
+
/**
|
|
18
|
+
* Check the current session state.
|
|
19
|
+
*
|
|
20
|
+
* Reads the token from localStorage — if present and not expired,
|
|
21
|
+
* returns user info from the server. Otherwise returns null.
|
|
22
|
+
*/
|
|
23
|
+
checkSession(): Observable<UserInfo | null>;
|
|
24
|
+
register(payload: {
|
|
25
|
+
email: string;
|
|
26
|
+
password: string;
|
|
27
|
+
teamName: string;
|
|
28
|
+
firstName: string;
|
|
29
|
+
lastName: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}): Observable<void>;
|
|
32
|
+
verify(email: string, token: string, delivery?: 'cookie' | 'fragment'): Observable<string>;
|
|
33
|
+
login(email: string, password: string, mode?: LoginMode): Observable<UserInfo>;
|
|
34
|
+
logout(): Observable<void>;
|
|
35
|
+
invite(email: string, role: 'owner' | 'member'): Observable<void>;
|
|
36
|
+
getInvitation(email: string, token: string): Observable<kit.Invitation>;
|
|
37
|
+
activate(payload: {
|
|
38
|
+
email: string;
|
|
39
|
+
token: string;
|
|
40
|
+
password: string;
|
|
41
|
+
}, mode?: LoginMode): Observable<void>;
|
|
42
|
+
acceptInvite(token: string): Observable<void>;
|
|
43
|
+
resendInvite(email: string): Observable<void>;
|
|
44
|
+
listInvitations(): Observable<PendingInvitation[]>;
|
|
45
|
+
loadTeams(): Observable<TeamMembership[]>;
|
|
46
|
+
switchTeam(teamId: {
|
|
47
|
+
$oid: string;
|
|
48
|
+
}, mode?: LoginMode): Observable<void>;
|
|
49
|
+
clearSession(): void;
|
|
50
|
+
forgotPassword(email: string): Observable<void>;
|
|
51
|
+
resetPassword(payload: {
|
|
52
|
+
email: string;
|
|
53
|
+
token: string;
|
|
54
|
+
password: string;
|
|
55
|
+
}, mode?: LoginMode): Observable<void>;
|
|
56
|
+
listTeamMembers(): Observable<TeamMember[]>;
|
|
57
|
+
removeMember(email: string): Observable<void>;
|
|
58
|
+
updateMemberRole(email: string, role: 'owner' | 'member'): Observable<void>;
|
|
59
|
+
createTeam(teamName: string): Observable<TeamMembership>;
|
|
60
|
+
updateTeam(updates: {
|
|
61
|
+
name?: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
}): Observable<void>;
|
|
64
|
+
deleteTeam(): Observable<void>;
|
|
65
|
+
updateProfile(updates: {
|
|
66
|
+
firstName?: string;
|
|
67
|
+
lastName?: string;
|
|
68
|
+
}): Observable<void>;
|
|
69
|
+
changePassword(currentPassword: string, newPassword: string): Observable<void>;
|
|
70
|
+
updateUser(email: string, updates: Record<string, unknown>): Observable<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Record the signed-in user's acceptance of the application's consents,
|
|
73
|
+
* renew the token so the guard sees it, and update the `user` signal.
|
|
74
|
+
*/
|
|
75
|
+
acceptConsents(body?: Record<string, unknown>, mode?: LoginMode): Observable<UserInfo>;
|
|
76
|
+
/** Force a new token, carrying the user document as it is now. */
|
|
77
|
+
renewToken(mode?: LoginMode): Observable<string | null>;
|
|
78
|
+
/**
|
|
79
|
+
* An authenticated `fetch` against the service, returned as an Observable.
|
|
80
|
+
*
|
|
81
|
+
* The Angular counterpart of React's `auth.api()` — the bearer token is
|
|
82
|
+
* attached automatically, and the request goes through the Angular
|
|
83
|
+
* interceptor chain (when `httpClientTransport` is configured).
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* this.auth.api('/demo').pipe(
|
|
87
|
+
* switchMap(res => res.json()),
|
|
88
|
+
* ).subscribe(data => console.log(data));
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* For a POST:
|
|
92
|
+
* ```ts
|
|
93
|
+
* this.auth.api('/demo', {
|
|
94
|
+
* method: 'POST',
|
|
95
|
+
* body: JSON.stringify({ name: 'hello' }),
|
|
96
|
+
* }).pipe(switchMap(res => res.json()));
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* Rejects with an `ApiError` (`{ status, message }`) on any non-2xx response.
|
|
100
|
+
*/
|
|
101
|
+
api(path: string, init?: RequestInit): Observable<Response>;
|
|
102
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RhAuthService, never>;
|
|
103
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Subscription, billing and order management.
|
|
107
|
+
*
|
|
108
|
+
* Separated from {@link RhAuthService} because payments are not authentication.
|
|
109
|
+
* Reads the current user from {@link RhAuthService} to derive
|
|
110
|
+
* `canManageBilling`, but owns its own subscription state.
|
|
111
|
+
*
|
|
112
|
+
* Subscription is loaded automatically when the user signs in and reloaded
|
|
113
|
+
* when they switch team (via an `effect` watching {@link teamKey}). No manual
|
|
114
|
+
* wiring needed.
|
|
115
|
+
*
|
|
116
|
+
* Only active when `config.payments` is `true` — otherwise every method
|
|
117
|
+
* is a no-op and no `/stripe/*` call is ever made.
|
|
118
|
+
*/
|
|
119
|
+
export declare class RhPaymentsService {
|
|
120
|
+
private readonly config;
|
|
121
|
+
private readonly auth;
|
|
122
|
+
private readonly _subscription;
|
|
123
|
+
readonly subscription: import("@angular/core").Signal<Subscription | null>;
|
|
124
|
+
readonly plan: import("@angular/core").Signal<string | null>;
|
|
125
|
+
readonly isSubscribed: import("@angular/core").Signal<boolean>;
|
|
126
|
+
readonly canManageBilling: import("@angular/core").Signal<boolean>;
|
|
127
|
+
readonly seatsAvailable: import("@angular/core").Signal<number | null>;
|
|
128
|
+
/**
|
|
129
|
+
* The team the subscription belongs to: `null` when signed out, `''` for a
|
|
130
|
+
* signed-in user with no team.
|
|
131
|
+
*
|
|
132
|
+
* The effect below tracks this rather than `auth.user()` itself. Both
|
|
133
|
+
* `updateProfile` and `acceptConsents` re-run `checkSession`, which replaces
|
|
134
|
+
* the user with a freshly parsed document — tracking that object's identity
|
|
135
|
+
* would reload the subscription on every profile edit and consent
|
|
136
|
+
* acceptance, which the adapter contract rules out. A subscription changes
|
|
137
|
+
* with the team, not with the profile.
|
|
138
|
+
*/
|
|
139
|
+
private readonly teamKey;
|
|
140
|
+
private get paymentsEnabled();
|
|
141
|
+
constructor();
|
|
142
|
+
/** Reload the team's subscription. No-op when `config.payments` is not `true`. */
|
|
143
|
+
loadSubscription(): Observable<Subscription | null>;
|
|
144
|
+
/** The service's subscription plan catalog. No session required. */
|
|
145
|
+
getPlans(): Observable<{
|
|
146
|
+
default_plan: string;
|
|
147
|
+
plans: Plan[];
|
|
148
|
+
}>;
|
|
149
|
+
/**
|
|
150
|
+
* Start a Stripe Checkout session.
|
|
151
|
+
*
|
|
152
|
+
* **Rejects with `status: 409`** when the team already has an active
|
|
153
|
+
* subscription — send them to {@link openBillingPortal} instead.
|
|
154
|
+
*/
|
|
155
|
+
createCheckoutSession(plan: string, interval: 'month' | 'year'): Observable<{
|
|
156
|
+
url: string;
|
|
157
|
+
}>;
|
|
158
|
+
/** Open the Stripe Customer Portal for self-service plan changes. */
|
|
159
|
+
openBillingPortal(): Observable<{
|
|
160
|
+
url: string;
|
|
161
|
+
}>;
|
|
162
|
+
/** The team's seat licences. Requires `canManageBilling`. */
|
|
163
|
+
getLicenses(): Observable<Licenses>;
|
|
164
|
+
/**
|
|
165
|
+
* Grant a seat licence. Returns `'granted'` or `'already-licensed'`.
|
|
166
|
+
* Rejects with `status: 409` when no seat is available.
|
|
167
|
+
*/
|
|
168
|
+
grantLicense(userId: string): Observable<GrantLicenseResult>;
|
|
169
|
+
/** Revoke a seat licence. */
|
|
170
|
+
revokeLicense(userId: string): Observable<void>;
|
|
171
|
+
/** Read the product catalog. */
|
|
172
|
+
getCatalog(opts?: CatalogQuery): Observable<CatalogItem[]>;
|
|
173
|
+
/**
|
|
174
|
+
* Create an order and start Checkout.
|
|
175
|
+
*
|
|
176
|
+
* @param email Required for guest checkout.
|
|
177
|
+
*/
|
|
178
|
+
createOrder(items: {
|
|
179
|
+
productId: string;
|
|
180
|
+
quantity: number;
|
|
181
|
+
}[], email?: string, collection?: string): Observable<{
|
|
182
|
+
_id: {
|
|
183
|
+
$oid: string;
|
|
184
|
+
};
|
|
185
|
+
checkout_url: string;
|
|
186
|
+
secret: string;
|
|
187
|
+
}>;
|
|
188
|
+
/** Read an order back. */
|
|
189
|
+
getOrder(id: string, secret?: string, collection?: string): Observable<Order>;
|
|
190
|
+
/**
|
|
191
|
+
* Poll until the subscription satisfies `predicate`.
|
|
192
|
+
*
|
|
193
|
+
* Use on the Checkout success page — the redirect back from Stripe races
|
|
194
|
+
* the webhook, so a bare `getSubscription` can still show the old plan.
|
|
195
|
+
*
|
|
196
|
+
* **Timing out is not a payment failure.** Rejects with
|
|
197
|
+
* `WaitTimeoutError` when the webhook hasn't arrived yet.
|
|
198
|
+
*/
|
|
199
|
+
waitForSubscription(predicate: (subscription: Subscription) => boolean, opts?: WaitOptions): Observable<Subscription>;
|
|
200
|
+
/**
|
|
201
|
+
* Poll until the order leaves `'pending_payment'`.
|
|
202
|
+
*
|
|
203
|
+
* Same race as {@link waitForSubscription} — use on the order success page.
|
|
204
|
+
*
|
|
205
|
+
* **Timing out is not a payment failure.** Rejects with
|
|
206
|
+
* `WaitTimeoutError` when the webhook hasn't arrived yet.
|
|
207
|
+
*/
|
|
208
|
+
waitForOrder(id: string, secret?: string, opts?: WaitOptions & {
|
|
209
|
+
collection?: string;
|
|
210
|
+
}): Observable<Order>;
|
|
211
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RhPaymentsService, never>;
|
|
212
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Where {@link RhCartService} keeps the cart in `localStorage`. Defaults to `'rh-cart'`.
|
|
216
|
+
*
|
|
217
|
+
* Worth providing when two of your apps share an origin, which is one deployment decision away
|
|
218
|
+
* from happening by accident.
|
|
219
|
+
*
|
|
220
|
+
* Declared here rather than in `tokens.ts`: that file is the authentication surface, and a cart
|
|
221
|
+
* needs no session. Importing the kit into it to reach one constant also dragged the kit's module
|
|
222
|
+
* into every spec that touches `RH_AUTH_CONFIG`, where it is auto-mocked — which shifted module
|
|
223
|
+
* initialisation enough to make two unrelated guard tests fail, in CI only.
|
|
224
|
+
*/
|
|
225
|
+
export declare const RH_CART_STORAGE_KEY: InjectionToken<string>;
|
|
226
|
+
/**
|
|
227
|
+
* The shopping cart.
|
|
228
|
+
*
|
|
229
|
+
* Independent of {@link import('./auth.service.js').RhAuthService | RhAuthService}
|
|
230
|
+
* and of `RH_AUTH_CONFIG`: a cart belongs to the browser, not to a session, and
|
|
231
|
+
* a shop that makes people sign in before they can put something in a basket
|
|
232
|
+
* loses most of them there. It becomes an order — which does need a config —
|
|
233
|
+
* when {@link RhCartService.orderItems} is handed to `createOrder`.
|
|
234
|
+
*
|
|
235
|
+
* On the server, `localStorage` does not exist and the cart reads as empty.
|
|
236
|
+
* That is correct for a rendered page — a request carries no basket — but it
|
|
237
|
+
* does mean a server-rendered cart count starts at zero and fills in once the
|
|
238
|
+
* browser takes over.
|
|
239
|
+
*/
|
|
240
|
+
export declare class RhCartService {
|
|
241
|
+
private readonly storageKey;
|
|
242
|
+
private readonly _lines;
|
|
243
|
+
/** The lines, in the order they were added. */
|
|
244
|
+
readonly lines: import("@angular/core").Signal<CartLine[]>;
|
|
245
|
+
private readonly totals;
|
|
246
|
+
/** Units, not lines: two of one thing counts two. */
|
|
247
|
+
readonly totalItems: import("@angular/core").Signal<number>;
|
|
248
|
+
/** Display only. Minor units, meaningful when every line shares a currency. */
|
|
249
|
+
readonly subtotal: import("@angular/core").Signal<number>;
|
|
250
|
+
/** The first line's currency, or `'eur'` when empty. */
|
|
251
|
+
readonly currency: import("@angular/core").Signal<string>;
|
|
252
|
+
/** The cart as `createOrder` wants it. */
|
|
253
|
+
readonly orderItems: import("@angular/core").Signal<import("@ulabase/kit").OrderItem[]>;
|
|
254
|
+
/** Adds an item, or increases the line already holding it. */
|
|
255
|
+
add(item: CartItem, quantity?: number): void;
|
|
256
|
+
/** Sets a line's quantity. Zero removes it. */
|
|
257
|
+
setQuantity(productId: string, quantity: number): void;
|
|
258
|
+
remove(productId: string): void;
|
|
259
|
+
clear(): void;
|
|
260
|
+
/**
|
|
261
|
+
* State and storage move together, in the operation that changed them.
|
|
262
|
+
*
|
|
263
|
+
* Deliberately not an `effect` reacting to the signal: an effect runs after
|
|
264
|
+
* the fact and can be skipped or reordered, and the failure that produces is
|
|
265
|
+
* a line coming back from storage after somebody removed it.
|
|
266
|
+
*/
|
|
267
|
+
private apply;
|
|
268
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RhCartService, never>;
|
|
269
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
270
|
+
}
|
|
271
|
+
export declare const authGuard: CanActivateFn;
|
|
272
|
+
export declare const publicGuard: CanActivateFn;
|
|
273
|
+
/**
|
|
274
|
+
* Authenticates the application's own `HttpClient` requests, and clears the
|
|
275
|
+
* session when the service rejects one with a 401.
|
|
276
|
+
*
|
|
277
|
+
* The kit's own calls go out through `fetch` and never reach an Angular
|
|
278
|
+
* interceptor, so without this an app querying its own collections had to
|
|
279
|
+
* attach the bearer token by hand at every call site.
|
|
280
|
+
*
|
|
281
|
+
* **Only requests to `apiBaseUrl` are touched.** The token is a credential:
|
|
282
|
+
* attaching it to every outgoing request would hand it to any third-party host
|
|
283
|
+
* the app happens to call. Requests elsewhere pass through untouched — they
|
|
284
|
+
* still get the 401 handling, which is about the session and not the target.
|
|
285
|
+
*
|
|
286
|
+
* An `Authorization` header already on the request is left alone, so a call
|
|
287
|
+
* that needs different credentials can say so.
|
|
288
|
+
*/
|
|
289
|
+
export declare const rhAuthInterceptor: HttpInterceptorFn;
|
|
290
|
+
/**
|
|
291
|
+
* A {@link AuthConfig.transport} backed by Angular's `HttpClient`, so the kit's
|
|
292
|
+
* own calls go through the interceptor chain like everything else the
|
|
293
|
+
* application sends.
|
|
294
|
+
*
|
|
295
|
+
* Without it the kit speaks `fetch` directly and an interceptor never sees a
|
|
296
|
+
* login, a session check or a token renewal — which makes any cross-cutting
|
|
297
|
+
* concern written as an interceptor quietly partial.
|
|
298
|
+
*
|
|
299
|
+
* Two mismatches with `fetch` are reconciled here:
|
|
300
|
+
*
|
|
301
|
+
* - **`HttpClient` throws on a non-2xx response.** `fetch` resolves and lets
|
|
302
|
+
* the caller read the status, which is what the core does to build its
|
|
303
|
+
* `ApiError`. So an `HttpErrorResponse` carrying a real response is turned
|
|
304
|
+
* back into a resolved `Response`.
|
|
305
|
+
* - **`status === 0` is not a response**, it is a network or CORS failure with
|
|
306
|
+
* nothing behind it. `new Response` would reject the status anyway, so the
|
|
307
|
+
* original error is rethrown and surfaces as a rejected promise, exactly as
|
|
308
|
+
* a failed `fetch` would.
|
|
309
|
+
*
|
|
310
|
+
* The body is read as text and left unparsed: the core calls `.json()` itself,
|
|
311
|
+
* and on an error response it needs the raw body to pull out the message.
|
|
312
|
+
*/
|
|
313
|
+
export declare function httpClientTransport(http: HttpClient): (url: string, init?: RequestInit) => Promise<Response>;
|
|
314
|
+
export declare function provideRhAuth(config: AuthConfig): EnvironmentProviders;
|
|
315
|
+
export declare const RH_AUTH_CONFIG: InjectionToken<AuthConfig>;
|
|
316
|
+
/**
|
|
317
|
+
* Marks a request as originating from the kit rather than from application
|
|
318
|
+
* code. Set by `httpClientTransport`, read by `rhAuthInterceptor`.
|
|
319
|
+
*
|
|
320
|
+
* The kit owns the meaning of a 401 on its own endpoints, and that meaning is
|
|
321
|
+
* not always "the session is dead": `PATCH /auth/change-password` answers 401
|
|
322
|
+
* when the *current password* is wrong, and `GET /token` when the credentials
|
|
323
|
+
* are. Letting the interceptor clear the session on those would sign a user
|
|
324
|
+
* out for mistyping their old password.
|
|
325
|
+
*/
|
|
326
|
+
declare const RH_KIT_REQUEST: HttpContextToken<boolean>;
|