@ratespecial/logto-angular 1.0.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 +507 -0
- package/fesm2022/ratespecial-logto-angular-testing.mjs +49 -0
- package/fesm2022/ratespecial-logto-angular-testing.mjs.map +1 -0
- package/fesm2022/ratespecial-logto-angular.mjs +396 -0
- package/fesm2022/ratespecial-logto-angular.mjs.map +1 -0
- package/package.json +42 -0
- package/types/ratespecial-logto-angular-testing.d.ts +15 -0
- package/types/ratespecial-logto-angular.d.ts +270 -0
package/README.md
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# @ratespecial/logto-angular
|
|
2
|
+
|
|
3
|
+
An Angular 21+ library that wraps [`@logto/browser`](https://docs.logto.io/sdk/browser) into an Angular-idiomatic authentication layer: a facade service, a route guard, two HTTP interceptors, callback and signed-out components, route-history tracking, a provider factory, and a test helper.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
- **Angular 21+ standalone** — no NgModules required.
|
|
10
|
+
- **Peer dependency on `@logto/browser` ^3.0.13** — one SDK client per app, shared across all resources.
|
|
11
|
+
- **Injection-token driven** — every configurable value flows through DI; no static singletons.
|
|
12
|
+
- **Signals + OnPush** — components use Angular signals; no Zone.js pressure.
|
|
13
|
+
- **Secondary entry point** — `@ratespecial/logto-angular/testing` ships test helpers separately so they are never bundled in production.
|
|
14
|
+
|
|
15
|
+
### Requirements
|
|
16
|
+
|
|
17
|
+
| Dependency | Version |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `@angular/core` | ^21.2.0 |
|
|
20
|
+
| `@angular/common` | ^21.2.0 |
|
|
21
|
+
| `@angular/router` | ^21.2.0 |
|
|
22
|
+
| `@logto/browser` | ^3.0.13 |
|
|
23
|
+
| `rxjs` | ~7.8.0 |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @ratespecial/logto-angular @logto/browser
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or with Yarn:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
yarn add @ratespecial/logto-angular @logto/browser
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
### 1. Wire providers in `app.config.ts`
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import {APP_INITIALIZER, ApplicationConfig, provideAppInitializer} from '@angular/core';
|
|
47
|
+
import {provideHttpClient, withInterceptors} from '@angular/common/http';
|
|
48
|
+
import {provideRouter} from '@angular/router';
|
|
49
|
+
import {
|
|
50
|
+
provideLogtoAuth,
|
|
51
|
+
logtoTokenInterceptor,
|
|
52
|
+
logoutOnUnauthInterceptor,
|
|
53
|
+
initializeRouteTracking,
|
|
54
|
+
} from '@ratespecial/logto-angular';
|
|
55
|
+
import {environment} from './environments/environment';
|
|
56
|
+
import {routes} from './app.routes';
|
|
57
|
+
|
|
58
|
+
export const appConfig: ApplicationConfig = {
|
|
59
|
+
providers: [
|
|
60
|
+
provideRouter(routes),
|
|
61
|
+
|
|
62
|
+
provideHttpClient(
|
|
63
|
+
withInterceptors([
|
|
64
|
+
logtoTokenInterceptor, // attaches Bearer tokens to matched routes
|
|
65
|
+
logoutOnUnauthInterceptor, // logs out on 401
|
|
66
|
+
]),
|
|
67
|
+
),
|
|
68
|
+
|
|
69
|
+
provideLogtoAuth({
|
|
70
|
+
...environment.logto,
|
|
71
|
+
logoutHookFactories: [
|
|
72
|
+
// optional: runs inside injection context, so inject() works here
|
|
73
|
+
// () => {
|
|
74
|
+
// const store = inject(Store);
|
|
75
|
+
// return () => store.dispatch(new ClearState());
|
|
76
|
+
// },
|
|
77
|
+
],
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
// Track routes so the user returns to where they were after login
|
|
81
|
+
provideAppInitializer(initializeRouteTracking()),
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Add auth routes in `app.routes.ts`
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import {Routes} from '@angular/router';
|
|
90
|
+
import {getAuthRoutes, authGuard} from '@ratespecial/logto-angular';
|
|
91
|
+
import {environment} from './environments/environment';
|
|
92
|
+
|
|
93
|
+
export const routes: Routes = [
|
|
94
|
+
{path: '', pathMatch: 'full', redirectTo: '/dashboard'},
|
|
95
|
+
|
|
96
|
+
// Spread the callback + signed-out routes from the lib
|
|
97
|
+
...getAuthRoutes(environment.logto.routing),
|
|
98
|
+
|
|
99
|
+
// Protect your app routes
|
|
100
|
+
{
|
|
101
|
+
path: 'dashboard',
|
|
102
|
+
loadComponent: () => import('./dashboard/dashboard.component'),
|
|
103
|
+
canActivate: [authGuard],
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Define the environment config
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// src/environments/environment.ts
|
|
112
|
+
import {UserScope} from '@logto/browser';
|
|
113
|
+
import {LogtoAuthConfig} from '@ratespecial/logto-angular';
|
|
114
|
+
|
|
115
|
+
export const environment = {
|
|
116
|
+
logto: {
|
|
117
|
+
endpoint: 'https://your-tenant.logto.app',
|
|
118
|
+
appId: 'your-app-id',
|
|
119
|
+
scopes: [UserScope.Email, UserScope.Profile, 'app:read'],
|
|
120
|
+
resources: ['https://api.yourapp.com'],
|
|
121
|
+
routing: {
|
|
122
|
+
callbackPath: '/auth/callback',
|
|
123
|
+
signedOutPath: '/auth/signed-out',
|
|
124
|
+
primaryResource: 'https://api.yourapp.com',
|
|
125
|
+
secureRoutes: [
|
|
126
|
+
{resource: 'https://api.yourapp.com', routes: ['/api']},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
} satisfies LogtoAuthConfig,
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Configuration reference
|
|
136
|
+
|
|
137
|
+
### `LogtoAuthConfig`
|
|
138
|
+
|
|
139
|
+
Extends `@logto/browser`'s native `LogtoConfig` with two extra fields:
|
|
140
|
+
|
|
141
|
+
| Field | Type | Required | Description |
|
|
142
|
+
|---|---|---|---|
|
|
143
|
+
| `endpoint` | `string` | Yes | Your Logto tenant URL. |
|
|
144
|
+
| `appId` | `string` | Yes | Application ID from the Logto console. |
|
|
145
|
+
| `scopes` | `string[]` | No | OIDC scopes to request. |
|
|
146
|
+
| `resources` | `string[]` | No | API resource indicators registered in Logto. |
|
|
147
|
+
| `routing` | `LogtoRoutingConfig` | Yes | Angular routing/behavior addon (see below). |
|
|
148
|
+
| `noAccessMessage` | `string` | No | Error shown when the user authenticates but has no scopes on the primary resource. |
|
|
149
|
+
|
|
150
|
+
### `LogtoRoutingConfig`
|
|
151
|
+
|
|
152
|
+
| Field | Type | Required | Description |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| `callbackPath` | `string` | Yes | Path Logto redirects to after sign-in (e.g. `/auth/callback`). |
|
|
155
|
+
| `signedOutPath` | `string` | Yes | Path shown after sign-out (e.g. `/auth/signed-out`). |
|
|
156
|
+
| `primaryResource` | `string` | No | The primary API resource indicator. Defaults to the first `secureRoutes` resource. Used for the scope-access gate in `CallbackComponent` and by consumers that need a specific resource token. |
|
|
157
|
+
| `secureRoutes` | `SecureRouteMapping[]` | Yes | Maps request URLs to resource tokens for the HTTP interceptor. |
|
|
158
|
+
|
|
159
|
+
### `SecureRouteMapping`
|
|
160
|
+
|
|
161
|
+
| Field | Type | Description |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `resource` | `string` | Logto API resource indicator to fetch a token for. |
|
|
164
|
+
| `routes` | `string[]` | Request URL prefixes (matched with `startsWith`) that require this resource's token. |
|
|
165
|
+
|
|
166
|
+
### `LogtoAuthOptions`
|
|
167
|
+
|
|
168
|
+
Passed to `provideLogtoAuth()`. Extends `LogtoAuthConfig` with:
|
|
169
|
+
|
|
170
|
+
| Field | Type | Description |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `logoutHookFactories` | `Array<() => AuthLogoutHook>` | Factories that run inside an injection context and return a hook to call at logout time. |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Feature guide
|
|
177
|
+
|
|
178
|
+
### `AuthService`
|
|
179
|
+
|
|
180
|
+
A singleton service (provided in root) that owns the authenticated state and all Logto operations.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import {AuthService} from '@ratespecial/logto-angular';
|
|
184
|
+
|
|
185
|
+
@Component({...})
|
|
186
|
+
export class MyComponent {
|
|
187
|
+
private auth = inject(AuthService);
|
|
188
|
+
|
|
189
|
+
constructor() {
|
|
190
|
+
// Subscribe to auth state changes
|
|
191
|
+
this.auth.isAuthenticated$.subscribe(authenticated => {
|
|
192
|
+
console.log('authenticated?', authenticated);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Public API:**
|
|
199
|
+
|
|
200
|
+
| Method/Property | Return | Description |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `isAuthenticated$` | `Observable<boolean>` | Replay-current stream; `distinctUntilChanged`. |
|
|
203
|
+
| `refreshAuthState()` | `Promise<boolean>` | Reads session from storage (no network); pushes result to stream. |
|
|
204
|
+
| `signIn(redirectUri?)` | `void` | Full-page redirect to Logto hosted UI. Default redirect URI is built from `callbackPath`. |
|
|
205
|
+
| `handleCallback(callbackUri)` | `Promise<void>` | Complete the OIDC callback, then refresh auth state. |
|
|
206
|
+
| `getAccessToken(resource?)` | `Promise<string>` | Resource-scoped JWT; client refreshes/caches transparently. |
|
|
207
|
+
| `getAccessTokenClaims(resource?)` | `Promise<AccessTokenClaims>` | Decoded claims of the resource token (e.g. `scope`). |
|
|
208
|
+
| `getIdTokenClaims()` | `Promise<IdTokenClaims>` | Decoded claims of the ID token (e.g. `sub`, `name`, `email`). |
|
|
209
|
+
| `logout()` | `void` | Fires hooks, emits `false`, calls `signOut`, navigates to `signedOutPath` on error. |
|
|
210
|
+
|
|
211
|
+
### `authGuard`
|
|
212
|
+
|
|
213
|
+
A functional `CanActivateFn` that checks for an active Logto session and redirects to the login flow when absent.
|
|
214
|
+
|
|
215
|
+
**Behavior:**
|
|
216
|
+
|
|
217
|
+
1. Calls `AuthService.refreshAuthState()`.
|
|
218
|
+
2. If authenticated, returns `true` (allows navigation).
|
|
219
|
+
3. If not authenticated:
|
|
220
|
+
- Saves the attempted URL via `HistoryService.setLastVisitedRoute()` (skipped for `/auth/*` paths to avoid loops).
|
|
221
|
+
- Calls `AuthService.signIn()` (full-page redirect).
|
|
222
|
+
- Returns `false`.
|
|
223
|
+
|
|
224
|
+
After sign-in, `CallbackComponent` restores navigation to the saved URL via `HistoryService.consumeLastVisitedRoute()`.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
// app.routes.ts
|
|
228
|
+
{
|
|
229
|
+
path: 'dashboard',
|
|
230
|
+
canActivate: [authGuard],
|
|
231
|
+
loadComponent: () => import('./dashboard/dashboard.component'),
|
|
232
|
+
},
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `logtoTokenInterceptor`
|
|
236
|
+
|
|
237
|
+
Attaches a resource-scoped `Bearer` token to outgoing HTTP requests that match a configured route prefix.
|
|
238
|
+
|
|
239
|
+
**How it works:**
|
|
240
|
+
|
|
241
|
+
- Reads `LOGTO_AUTH_CONFIG.routing.secureRoutes` at intercept time.
|
|
242
|
+
- Calls `resourceForUrl(req.url, secureRoutes)` to find the first mapping whose `routes` entry matches the request URL via `startsWith`.
|
|
243
|
+
- If a resource matches, calls `AuthService.getAccessToken(resource)` (the Logto SDK refreshes/caches the token).
|
|
244
|
+
- Adds `Authorization: Bearer <token>` and forwards the cloned request.
|
|
245
|
+
- If no resource matches, or the token is empty, the request passes through unchanged.
|
|
246
|
+
|
|
247
|
+
**Using `resourceForUrl` standalone:**
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import {resourceForUrl} from '@ratespecial/logto-angular';
|
|
251
|
+
|
|
252
|
+
const resource = resourceForUrl('/api/v2/users', secureRoutes);
|
|
253
|
+
// 'https://api.yourapp.com' (or undefined)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Multi-resource example:**
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
routing: {
|
|
260
|
+
secureRoutes: [
|
|
261
|
+
{resource: 'https://api.yourapp.com', routes: ['/api']},
|
|
262
|
+
{resource: 'https://billing.yourapp.com', routes: ['/billing', '/payments']},
|
|
263
|
+
],
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Requests to `/api/...` get a token for `https://api.yourapp.com`; requests to `/billing/...` or `/payments/...` get a token for `https://billing.yourapp.com`.
|
|
268
|
+
|
|
269
|
+
### `logoutOnUnauthInterceptor`
|
|
270
|
+
|
|
271
|
+
Calls `AuthService.logout()` whenever any HTTP response returns `401 Unauthorized`.
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
provideHttpClient(
|
|
275
|
+
withInterceptors([logtoTokenInterceptor, logoutOnUnauthInterceptor]),
|
|
276
|
+
),
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Order matters: place `logtoTokenInterceptor` first so the token is attached before the response interceptor evaluates it.
|
|
280
|
+
|
|
281
|
+
### `CallbackComponent` and `SignedOutComponent`
|
|
282
|
+
|
|
283
|
+
These are registered automatically by `getAuthRoutes()`:
|
|
284
|
+
|
|
285
|
+
- **`CallbackComponent`** (`lib-callback`): Handles the OIDC redirect. Calls `handleCallback`, checks that the primary resource token carries scopes (access gate), then navigates to the restored route or `/`. Displays a spinner while loading and an error message on failure or no-access.
|
|
286
|
+
- **`SignedOutComponent`** (`lib-signed-out`): Shows a "Signed out" card with a "Sign in again" button that restarts the Logto flow.
|
|
287
|
+
|
|
288
|
+
Both use `ChangeDetectionStrategy.OnPush` and signals.
|
|
289
|
+
|
|
290
|
+
### `HistoryService`
|
|
291
|
+
|
|
292
|
+
Persists the last visited route in `sessionStorage` across the OIDC redirect round-trip.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import {HistoryService} from '@ratespecial/logto-angular';
|
|
296
|
+
|
|
297
|
+
@Injectable({providedIn: 'root'})
|
|
298
|
+
export class MyService {
|
|
299
|
+
private history = inject(HistoryService);
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
| Method | Description |
|
|
304
|
+
|---|---|
|
|
305
|
+
| `setLastVisitedRoute(route)` | Writes to `sessionStorage` (key: `auth.lastVisitedRoute`). |
|
|
306
|
+
| `getLastVisitedRoute()` | Reads from `sessionStorage`; returns `null` if absent. |
|
|
307
|
+
| `clearLastVisitedRoute()` | Removes the stored value. |
|
|
308
|
+
| `consumeLastVisitedRoute()` | Reads then clears in one call. Used by `CallbackComponent`. |
|
|
309
|
+
|
|
310
|
+
All methods swallow `sessionStorage` errors (e.g. quota exceeded) and log them to `console.error`.
|
|
311
|
+
|
|
312
|
+
### `initializeRouteTracking`
|
|
313
|
+
|
|
314
|
+
An `APP_INITIALIZER` factory that subscribes to `Router.events` and records each non-auth route via `HistoryService`. Routes starting with `/auth` are excluded to prevent redirect loops.
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import {provideAppInitializer} from '@angular/core';
|
|
318
|
+
import {initializeRouteTracking} from '@ratespecial/logto-angular';
|
|
319
|
+
|
|
320
|
+
providers: [
|
|
321
|
+
provideAppInitializer(initializeRouteTracking()),,
|
|
322
|
+
]
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Logout hooks
|
|
326
|
+
|
|
327
|
+
Register side-effects that run at the start of `AuthService.logout()` before the sign-out redirect. Common uses: clearing NGXS/NgRx state, flushing caches, firing telemetry events.
|
|
328
|
+
|
|
329
|
+
Each factory receives an injection context (DI works inside it), and returns an `AuthLogoutHook` — a function that returns `void` or `Observable<unknown>`. Async hooks are fire-and-forget.
|
|
330
|
+
|
|
331
|
+
**Via `provideLogtoAuth` (recommended):**
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
provideLogtoAuth({
|
|
335
|
+
...environment.logto,
|
|
336
|
+
logoutHookFactories: [
|
|
337
|
+
() => {
|
|
338
|
+
const store = inject(Store); // inject() works here
|
|
339
|
+
return () => store.dispatch(new ClearState()); // returns Observable
|
|
340
|
+
},
|
|
341
|
+
() => {
|
|
342
|
+
const cache = inject(CacheService);
|
|
343
|
+
return () => cache.clear(); // returns void
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
})
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Direct token registration (advanced):**
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
{provide: AUTH_LOGOUT_HOOK, multi: true, useFactory: () => {
|
|
353
|
+
const store = inject(Store);
|
|
354
|
+
return () => store.dispatch(new ClearState());
|
|
355
|
+
}}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Injection tokens
|
|
359
|
+
|
|
360
|
+
| Token | Type | Provided by | Consumed by |
|
|
361
|
+
|---|---|---|---|
|
|
362
|
+
| `LOGTO_AUTH_CONFIG` | `LogtoAuthConfig` | `provideLogtoAuth()` | `AuthService`, `logtoTokenInterceptor`, `CallbackComponent` |
|
|
363
|
+
| `LOGTO_CLIENT` | `LogtoClient` | `provideLogtoAuth()` | `AuthService` |
|
|
364
|
+
| `PRIMARY_RESOURCE` | `string` | `provideLogtoAuth()` | `CallbackComponent`, app components needing a specific token |
|
|
365
|
+
| `AUTH_LOGOUT_HOOK` | `AuthLogoutHook[]` | `provideLogtoAuth()` (multi) | `AuthService.logout()` |
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Testing
|
|
370
|
+
|
|
371
|
+
Use `provideLogtoTesting()` from the secondary entry point to inject test doubles without a real Logto setup.
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import {provideLogtoTesting} from '@ratespecial/logto-angular/testing';
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Default behaviour:** the stub client always returns unauthenticated with an empty access token and empty scope claims. The default config uses `https://api.example.test` as the primary resource.
|
|
378
|
+
|
|
379
|
+
**Basic usage:**
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
TestBed.configureTestingModule({
|
|
383
|
+
providers: [
|
|
384
|
+
...provideLogtoTesting(),
|
|
385
|
+
MyComponent,
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Override client methods:**
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
TestBed.configureTestingModule({
|
|
394
|
+
providers: [
|
|
395
|
+
...provideLogtoTesting({
|
|
396
|
+
isAuthenticated: async () => true,
|
|
397
|
+
getAccessToken: async () => 'my-test-token',
|
|
398
|
+
getAccessTokenClaims: async () => ({scope: 'read write'}),
|
|
399
|
+
}),
|
|
400
|
+
],
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Override config:**
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
TestBed.configureTestingModule({
|
|
408
|
+
providers: [
|
|
409
|
+
...provideLogtoTesting(
|
|
410
|
+
{}, // no client overrides
|
|
411
|
+
{
|
|
412
|
+
noAccessMessage: 'Custom access-denied message.',
|
|
413
|
+
routing: {
|
|
414
|
+
callbackPath: '/custom/callback',
|
|
415
|
+
signedOutPath: '/custom/signed-out',
|
|
416
|
+
primaryResource: 'https://api.custom.test',
|
|
417
|
+
secureRoutes: [{resource: 'https://api.custom.test', routes: ['/api']}],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
),
|
|
421
|
+
],
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Building and local linking
|
|
428
|
+
|
|
429
|
+
### Build the library
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
# From the logto-angular workspace root
|
|
433
|
+
ng build logto-angular
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Output is emitted to `dist/logto-angular` (with `dist/logto-angular/testing` for the secondary entry point).
|
|
437
|
+
|
|
438
|
+
### Link to a consuming app (Yarn `file:`)
|
|
439
|
+
|
|
440
|
+
Use Yarn's `file:` protocol — **not** `portal:` or `link:` — to reference the built dist.
|
|
441
|
+
|
|
442
|
+
In the consuming app's `package.json`:
|
|
443
|
+
|
|
444
|
+
```json
|
|
445
|
+
{
|
|
446
|
+
"dependencies": {
|
|
447
|
+
"@ratespecial/logto-angular": "file:../../logto-angular/dist/logto-angular"
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Then install:
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
yarn install
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Why `file:` and not `portal:`?**
|
|
459
|
+
|
|
460
|
+
`portal:` creates a symlink whose *real* path still sits inside the `logto-angular` workspace. Node.js and esbuild follow the symlink back to the real location, then walk up the directory tree and find `/home/fish/logto-angular/node_modules/@angular` — a second Angular instance alongside the consumer's own. This causes `NG0203` injection errors at runtime and structural type-incompatibility errors at compile time (the two Angular copies produce distinct class definitions even at the same version).
|
|
461
|
+
|
|
462
|
+
`file:` snapshots the dist as a real directory copy under the consumer's `node_modules`. Peer-dependency resolution then finds only the consumer's single Angular, eliminating the dual-instance problem entirely.
|
|
463
|
+
|
|
464
|
+
**After each library rebuild, re-run `yarn install` in the consumer** to pick up the latest dist snapshot — `file:` does not live-update the way a symlink does:
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
# Terminal 1 — rebuild library
|
|
468
|
+
ng build logto-angular
|
|
469
|
+
|
|
470
|
+
# Terminal 2 — re-snapshot in consumer
|
|
471
|
+
yarn install # or: yarn up @ratespecial/logto-angular
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**Belt-and-suspenders: `preserveSymlinks`**
|
|
475
|
+
|
|
476
|
+
Add `preserveSymlinks: true` to the consumer's Angular build target in `angular.json` and to its `tsconfig.json` `compilerOptions`. This prevents the Angular compiler from canonicalizing any remaining symlinks and ensures module resolution stays within the consumer's `node_modules`:
|
|
477
|
+
|
|
478
|
+
```json
|
|
479
|
+
// angular.json — inside the build target's options
|
|
480
|
+
{
|
|
481
|
+
"preserveSymlinks": true
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
```json
|
|
486
|
+
// tsconfig.json
|
|
487
|
+
{
|
|
488
|
+
"compilerOptions": {
|
|
489
|
+
"preserveSymlinks": true
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Run tests
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
ng test
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Publish
|
|
501
|
+
|
|
502
|
+
Update `version` in `projects/logto-angular/package.json`, then:
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
ng build logto-angular
|
|
506
|
+
npm publish dist/logto-angular --access restricted
|
|
507
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { LOGTO_CLIENT, LOGTO_AUTH_CONFIG, PRIMARY_RESOURCE } from '@ratespecial/logto-angular';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_PRIMARY_RESOURCE = 'https://api.example.test';
|
|
4
|
+
const DEFAULT_TEST_CONFIG = {
|
|
5
|
+
endpoint: 'https://test.logto.app',
|
|
6
|
+
appId: 'test-app',
|
|
7
|
+
routing: {
|
|
8
|
+
callbackPath: '/auth/callback',
|
|
9
|
+
signedOutPath: '/auth/signed-out',
|
|
10
|
+
primaryResource: DEFAULT_PRIMARY_RESOURCE,
|
|
11
|
+
secureRoutes: [{ resource: DEFAULT_PRIMARY_RESOURCE, routes: ['/api'] }],
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Test doubles for the auth layer. Provides a no-op `LogtoClient` (unauthenticated by default)
|
|
16
|
+
* plus the `LOGTO_AUTH_CONFIG` / `PRIMARY_RESOURCE` tokens that `AuthService` and the auth
|
|
17
|
+
* components depend on, so components under test can inject them without a real Logto setup.
|
|
18
|
+
*
|
|
19
|
+
* @param clientOverrides - Partial `LogtoClient` methods to override on the stub.
|
|
20
|
+
* @param configOverrides - Partial `LogtoAuthConfig` fields to override on the default test config.
|
|
21
|
+
*/
|
|
22
|
+
function provideLogtoTesting(clientOverrides = {}, configOverrides = {}) {
|
|
23
|
+
const config = { ...DEFAULT_TEST_CONFIG, ...configOverrides };
|
|
24
|
+
const primaryResource = config.routing.primaryResource ??
|
|
25
|
+
config.routing.secureRoutes[0]?.resource ??
|
|
26
|
+
DEFAULT_PRIMARY_RESOURCE;
|
|
27
|
+
const stub = {
|
|
28
|
+
isAuthenticated: async () => false,
|
|
29
|
+
getAccessToken: async () => '',
|
|
30
|
+
getAccessTokenClaims: async () => ({ scope: '' }),
|
|
31
|
+
getIdTokenClaims: async () => ({}),
|
|
32
|
+
signIn: async () => undefined,
|
|
33
|
+
signOut: async () => undefined,
|
|
34
|
+
handleSignInCallback: async () => undefined,
|
|
35
|
+
...clientOverrides,
|
|
36
|
+
};
|
|
37
|
+
return [
|
|
38
|
+
{ provide: LOGTO_CLIENT, useValue: stub },
|
|
39
|
+
{ provide: LOGTO_AUTH_CONFIG, useValue: config },
|
|
40
|
+
{ provide: PRIMARY_RESOURCE, useValue: primaryResource },
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generated bundle index. Do not edit.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
export { provideLogtoTesting };
|
|
49
|
+
//# sourceMappingURL=ratespecial-logto-angular-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ratespecial-logto-angular-testing.mjs","sources":["../../../projects/logto-angular/testing/src/provide-logto-testing.ts","../../../projects/logto-angular/testing/src/ratespecial-logto-angular-testing.ts"],"sourcesContent":["import { Provider } from '@angular/core';\nimport type LogtoClient from '@logto/browser';\nimport type { LogtoAuthConfig } from '@ratespecial/logto-angular';\nimport { LOGTO_AUTH_CONFIG, LOGTO_CLIENT, PRIMARY_RESOURCE } from '@ratespecial/logto-angular';\n\nconst DEFAULT_PRIMARY_RESOURCE = 'https://api.example.test';\n\nconst DEFAULT_TEST_CONFIG: LogtoAuthConfig = {\n endpoint: 'https://test.logto.app',\n appId: 'test-app',\n routing: {\n callbackPath: '/auth/callback',\n signedOutPath: '/auth/signed-out',\n primaryResource: DEFAULT_PRIMARY_RESOURCE,\n secureRoutes: [{ resource: DEFAULT_PRIMARY_RESOURCE, routes: ['/api'] }],\n },\n};\n\n/**\n * Test doubles for the auth layer. Provides a no-op `LogtoClient` (unauthenticated by default)\n * plus the `LOGTO_AUTH_CONFIG` / `PRIMARY_RESOURCE` tokens that `AuthService` and the auth\n * components depend on, so components under test can inject them without a real Logto setup.\n *\n * @param clientOverrides - Partial `LogtoClient` methods to override on the stub.\n * @param configOverrides - Partial `LogtoAuthConfig` fields to override on the default test config.\n */\nexport function provideLogtoTesting(\n clientOverrides: Partial<LogtoClient> = {},\n configOverrides: Partial<LogtoAuthConfig> = {},\n): Provider[] {\n const config: LogtoAuthConfig = { ...DEFAULT_TEST_CONFIG, ...configOverrides };\n const primaryResource =\n config.routing.primaryResource ??\n config.routing.secureRoutes[0]?.resource ??\n DEFAULT_PRIMARY_RESOURCE;\n\n const stub = {\n isAuthenticated: async () => false,\n getAccessToken: async () => '',\n getAccessTokenClaims: async () => ({ scope: '' }),\n getIdTokenClaims: async () => ({}),\n signIn: async () => undefined,\n signOut: async () => undefined,\n handleSignInCallback: async () => undefined,\n ...clientOverrides,\n } as unknown as LogtoClient;\n\n return [\n { provide: LOGTO_CLIENT, useValue: stub },\n { provide: LOGTO_AUTH_CONFIG, useValue: config },\n { provide: PRIMARY_RESOURCE, useValue: primaryResource },\n ];\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAKA,MAAM,wBAAwB,GAAG,0BAA0B;AAE3D,MAAM,mBAAmB,GAAoB;AAC3C,IAAA,QAAQ,EAAE,wBAAwB;AAClC,IAAA,KAAK,EAAE,UAAU;AACjB,IAAA,OAAO,EAAE;AACP,QAAA,YAAY,EAAE,gBAAgB;AAC9B,QAAA,aAAa,EAAE,kBAAkB;AACjC,QAAA,eAAe,EAAE,wBAAwB;AACzC,QAAA,YAAY,EAAE,CAAC,EAAE,QAAQ,EAAE,wBAAwB,EAAE,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;AACzE,KAAA;CACF;AAED;;;;;;;AAOG;SACa,mBAAmB,CACjC,kBAAwC,EAAE,EAC1C,kBAA4C,EAAE,EAAA;IAE9C,MAAM,MAAM,GAAoB,EAAE,GAAG,mBAAmB,EAAE,GAAG,eAAe,EAAE;AAC9E,IAAA,MAAM,eAAe,GACnB,MAAM,CAAC,OAAO,CAAC,eAAe;QAC9B,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,QAAQ;AACxC,QAAA,wBAAwB;AAE1B,IAAA,MAAM,IAAI,GAAG;AACX,QAAA,eAAe,EAAE,YAAY,KAAK;AAClC,QAAA,cAAc,EAAE,YAAY,EAAE;QAC9B,oBAAoB,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AACjD,QAAA,gBAAgB,EAAE,aAAa,EAAE,CAAC;AAClC,QAAA,MAAM,EAAE,YAAY,SAAS;AAC7B,QAAA,OAAO,EAAE,YAAY,SAAS;AAC9B,QAAA,oBAAoB,EAAE,YAAY,SAAS;AAC3C,QAAA,GAAG,eAAe;KACO;IAE3B,OAAO;AACL,QAAA,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE;AACzC,QAAA,EAAE,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,EAAE;AAChD,QAAA,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,eAAe,EAAE;KACzD;AACH;;ACpDA;;AAEG;;;;"}
|