@acontplus/ng-auth 1.1.2 → 1.1.3
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/fesm2022/acontplus-ng-auth.mjs +327 -287
- package/fesm2022/acontplus-ng-auth.mjs.map +1 -1
- package/index.d.ts +126 -105
- package/package.json +1 -1
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { UserRepository, BaseUseCase,
|
|
1
|
+
import { UserRepository, BaseUseCase, TOKEN_PROVIDER, LoggingService } from '@acontplus/ng-infrastructure';
|
|
2
2
|
export { TOKEN_PROVIDER } from '@acontplus/ng-infrastructure';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
4
|
import { inject, PLATFORM_ID, Injectable, NgZone, signal, input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
5
5
|
import { Router } from '@angular/router';
|
|
6
|
+
import { of, tap, catchError, throwError, firstValueFrom, map, from } from 'rxjs';
|
|
6
7
|
import * as i1 from '@angular/common';
|
|
7
8
|
import { isPlatformBrowser, DOCUMENT, CommonModule } from '@angular/common';
|
|
8
9
|
import { jwtDecode } from 'jwt-decode';
|
|
9
10
|
import { ENVIRONMENT, AUTH_API } from '@acontplus/ng-config';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { HttpClient } from '@angular/common/http';
|
|
11
|
+
import { HttpClient, HttpContextToken } from '@angular/common/http';
|
|
12
|
+
import { catchError as catchError$1, switchMap } from 'rxjs/operators';
|
|
13
13
|
import * as i2 from '@angular/forms';
|
|
14
14
|
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
15
15
|
import { MatCard, MatCardHeader, MatCardTitle, MatCardContent, MatCardFooter } from '@angular/material/card';
|
|
@@ -19,6 +19,9 @@ import { MatIcon } from '@angular/material/icon';
|
|
|
19
19
|
import { MatButton, MatAnchor } from '@angular/material/button';
|
|
20
20
|
import { MatCheckbox } from '@angular/material/checkbox';
|
|
21
21
|
|
|
22
|
+
class AuthRepository {
|
|
23
|
+
}
|
|
24
|
+
|
|
22
25
|
class TokenRepository {
|
|
23
26
|
environment = inject(ENVIRONMENT);
|
|
24
27
|
platformId = inject(PLATFORM_ID);
|
|
@@ -137,271 +140,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
137
140
|
}]
|
|
138
141
|
}] });
|
|
139
142
|
|
|
140
|
-
/**
|
|
141
|
-
* Service to manage URL redirection after authentication
|
|
142
|
-
* Stores the intended URL when session is lost and redirects to it after successful login
|
|
143
|
-
* SSR-compatible by checking platform before accessing sessionStorage
|
|
144
|
-
*/
|
|
145
|
-
class UrlRedirectService {
|
|
146
|
-
REDIRECT_URL_KEY = 'acp_redirect_url';
|
|
147
|
-
EXCLUDED_ROUTES = [
|
|
148
|
-
'/login',
|
|
149
|
-
'/auth',
|
|
150
|
-
'/register',
|
|
151
|
-
'/forgot-password',
|
|
152
|
-
'/reset-password',
|
|
153
|
-
];
|
|
154
|
-
router = inject(Router);
|
|
155
|
-
platformId = inject(PLATFORM_ID);
|
|
156
|
-
document = inject(DOCUMENT);
|
|
157
|
-
/**
|
|
158
|
-
* Stores the current URL for later redirection
|
|
159
|
-
* @param url - The URL to store (defaults to current URL)
|
|
160
|
-
*/
|
|
161
|
-
storeIntendedUrl(url) {
|
|
162
|
-
// Only store in browser environment
|
|
163
|
-
if (!this.isBrowser()) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const urlToStore = url || this.router.url;
|
|
167
|
-
// Don't store authentication-related routes
|
|
168
|
-
if (this.isExcludedRoute(urlToStore)) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
// Don't store URLs with query parameters that might contain sensitive data
|
|
172
|
-
const urlWithoutParams = urlToStore.split('?')[0];
|
|
173
|
-
this.getSessionStorage()?.setItem(this.REDIRECT_URL_KEY, urlWithoutParams);
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Gets the stored intended URL
|
|
177
|
-
* @returns The stored URL or null if none exists
|
|
178
|
-
*/
|
|
179
|
-
getIntendedUrl() {
|
|
180
|
-
if (!this.isBrowser()) {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
return this.getSessionStorage()?.getItem(this.REDIRECT_URL_KEY) || null;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Redirects to the stored URL and clears it from storage
|
|
187
|
-
* @param defaultRoute - The default route to navigate to if no URL is stored
|
|
188
|
-
*/
|
|
189
|
-
redirectToIntendedUrl(defaultRoute = '/') {
|
|
190
|
-
const intendedUrl = this.getIntendedUrl();
|
|
191
|
-
if (intendedUrl && !this.isExcludedRoute(intendedUrl)) {
|
|
192
|
-
this.clearIntendedUrl();
|
|
193
|
-
this.router.navigateByUrl(intendedUrl);
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
this.router.navigate([defaultRoute]);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Clears the stored intended URL
|
|
201
|
-
*/
|
|
202
|
-
clearIntendedUrl() {
|
|
203
|
-
if (!this.isBrowser()) {
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
this.getSessionStorage()?.removeItem(this.REDIRECT_URL_KEY);
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Checks if a URL should be excluded from redirection
|
|
210
|
-
* @param url - The URL to check
|
|
211
|
-
* @returns True if the URL should be excluded
|
|
212
|
-
*/
|
|
213
|
-
isExcludedRoute(url) {
|
|
214
|
-
return this.EXCLUDED_ROUTES.some(route => url.includes(route));
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Stores the current URL if it's not an excluded route
|
|
218
|
-
* Useful for guards and interceptors
|
|
219
|
-
*/
|
|
220
|
-
storeCurrentUrlIfAllowed() {
|
|
221
|
-
const currentUrl = this.router.url;
|
|
222
|
-
if (!this.isExcludedRoute(currentUrl)) {
|
|
223
|
-
this.storeIntendedUrl(currentUrl);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Checks if we're running in a browser environment
|
|
228
|
-
* @returns True if running in browser, false if SSR
|
|
229
|
-
*/
|
|
230
|
-
isBrowser() {
|
|
231
|
-
return isPlatformBrowser(this.platformId);
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Safely gets sessionStorage reference
|
|
235
|
-
* @returns sessionStorage object or null if not available
|
|
236
|
-
*/
|
|
237
|
-
getSessionStorage() {
|
|
238
|
-
if (!this.isBrowser()) {
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
return this.document.defaultView?.sessionStorage || null;
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
// Handle cases where sessionStorage might be disabled
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UrlRedirectService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
250
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UrlRedirectService, providedIn: 'root' });
|
|
251
|
-
}
|
|
252
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UrlRedirectService, decorators: [{
|
|
253
|
-
type: Injectable,
|
|
254
|
-
args: [{
|
|
255
|
-
providedIn: 'root',
|
|
256
|
-
}]
|
|
257
|
-
}] });
|
|
258
|
-
|
|
259
|
-
const authGuard = (_route, state) => {
|
|
260
|
-
const tokenRepository = inject(TokenRepository);
|
|
261
|
-
const router = inject(Router);
|
|
262
|
-
const urlRedirectService = inject(UrlRedirectService);
|
|
263
|
-
const environment = inject(ENVIRONMENT);
|
|
264
|
-
if (tokenRepository.isAuthenticated()) {
|
|
265
|
-
return true;
|
|
266
|
-
}
|
|
267
|
-
// Store the current URL for redirection after login
|
|
268
|
-
urlRedirectService.storeIntendedUrl(state.url);
|
|
269
|
-
// Redirect to login page (configurable via environment)
|
|
270
|
-
router.navigate([environment.loginRoute]);
|
|
271
|
-
return false;
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Interceptor that handles authentication errors and manages URL redirection
|
|
276
|
-
* Captures the current URL when a 401 error occurs and redirects to login
|
|
277
|
-
*/
|
|
278
|
-
const authRedirectInterceptor = (req, next) => {
|
|
279
|
-
const router = inject(Router);
|
|
280
|
-
const urlRedirectService = inject(UrlRedirectService);
|
|
281
|
-
const tokenRepository = inject(TokenRepository);
|
|
282
|
-
const environment = inject(ENVIRONMENT);
|
|
283
|
-
return next(req).pipe(catchError((error) => {
|
|
284
|
-
// Handle 401 Unauthorized errors
|
|
285
|
-
if (error.status === 401) {
|
|
286
|
-
// Only store and redirect if user was previously authenticated
|
|
287
|
-
// This prevents redirect loops and handles session expiry scenarios
|
|
288
|
-
if (tokenRepository.isAuthenticated()) {
|
|
289
|
-
// Store the current URL for redirection after re-authentication
|
|
290
|
-
urlRedirectService.storeCurrentUrlIfAllowed();
|
|
291
|
-
// Navigate to login page
|
|
292
|
-
router.navigate([environment.loginRoute]);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// Re-throw the error so other error handlers can process it
|
|
296
|
-
return throwError(() => error);
|
|
297
|
-
}));
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// src/lib/domain/models/auth.ts
|
|
301
|
-
|
|
302
|
-
// src/lib/domain/models/index.ts
|
|
303
|
-
|
|
304
|
-
class AuthRepository {
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// src/lib/domain/repositories/index.ts
|
|
308
|
-
|
|
309
|
-
// src/lib/domain/index.ts
|
|
310
|
-
|
|
311
|
-
// src/lib/services/csrf.service.ts
|
|
312
|
-
class CsrfService {
|
|
313
|
-
http = inject(HttpClient);
|
|
314
|
-
csrfToken = null;
|
|
315
|
-
/**
|
|
316
|
-
* Get CSRF token, fetching it if not available
|
|
317
|
-
*/
|
|
318
|
-
async getCsrfToken() {
|
|
319
|
-
if (this.csrfToken) {
|
|
320
|
-
return this.csrfToken;
|
|
321
|
-
}
|
|
322
|
-
try {
|
|
323
|
-
this.csrfToken = await firstValueFrom(this.http
|
|
324
|
-
.get('/csrf-token')
|
|
325
|
-
.pipe(map(response => response.csrfToken)));
|
|
326
|
-
return this.csrfToken || '';
|
|
327
|
-
}
|
|
328
|
-
catch {
|
|
329
|
-
// If CSRF endpoint fails, return empty token
|
|
330
|
-
// Server should handle missing CSRF tokens appropriately
|
|
331
|
-
return '';
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Clear stored CSRF token (useful on logout)
|
|
336
|
-
*/
|
|
337
|
-
clearCsrfToken() {
|
|
338
|
-
this.csrfToken = null;
|
|
339
|
-
}
|
|
340
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: CsrfService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
341
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: CsrfService, providedIn: 'root' });
|
|
342
|
-
}
|
|
343
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: CsrfService, decorators: [{
|
|
344
|
-
type: Injectable,
|
|
345
|
-
args: [{
|
|
346
|
-
providedIn: 'root',
|
|
347
|
-
}]
|
|
348
|
-
}] });
|
|
349
|
-
|
|
350
|
-
// src/lib/data/repositories/auth-http.repository.ts
|
|
351
|
-
function getDeviceInfo() {
|
|
352
|
-
return `${navigator.platform ?? 'Unknown'} - ${navigator.userAgent}`;
|
|
353
|
-
}
|
|
354
|
-
class AuthHttpRepository extends AuthRepository {
|
|
355
|
-
http = inject(HttpClient);
|
|
356
|
-
csrfService = inject(CsrfService);
|
|
357
|
-
URL = `${AUTH_API.ACCOUNT}`;
|
|
358
|
-
login(request) {
|
|
359
|
-
return from(this.csrfService.getCsrfToken()).pipe(switchMap(csrfToken => this.http.post(`${this.URL}login`, request, {
|
|
360
|
-
headers: {
|
|
361
|
-
'Device-Info': getDeviceInfo(),
|
|
362
|
-
'X-CSRF-Token': csrfToken,
|
|
363
|
-
},
|
|
364
|
-
withCredentials: true,
|
|
365
|
-
})));
|
|
366
|
-
}
|
|
367
|
-
register(request) {
|
|
368
|
-
return from(this.csrfService.getCsrfToken()).pipe(switchMap(csrfToken => this.http.post(`${this.URL}register`, request, {
|
|
369
|
-
headers: {
|
|
370
|
-
'Device-Info': getDeviceInfo(),
|
|
371
|
-
'X-CSRF-Token': csrfToken,
|
|
372
|
-
},
|
|
373
|
-
withCredentials: true,
|
|
374
|
-
})));
|
|
375
|
-
}
|
|
376
|
-
refreshToken(request) {
|
|
377
|
-
return from(this.csrfService.getCsrfToken()).pipe(switchMap(csrfToken => this.http.post(`${this.URL}refresh`, request, {
|
|
378
|
-
headers: {
|
|
379
|
-
'Device-Info': getDeviceInfo(),
|
|
380
|
-
'X-CSRF-Token': csrfToken,
|
|
381
|
-
},
|
|
382
|
-
withCredentials: true,
|
|
383
|
-
})));
|
|
384
|
-
}
|
|
385
|
-
logout(email, refreshToken) {
|
|
386
|
-
return from(this.csrfService.getCsrfToken()).pipe(switchMap(csrfToken => this.http.post(`${this.URL}logout`, { email, refreshToken: refreshToken || undefined }, {
|
|
387
|
-
headers: { 'X-CSRF-Token': csrfToken },
|
|
388
|
-
withCredentials: true, // Ensure cookies are sent
|
|
389
|
-
})));
|
|
390
|
-
}
|
|
391
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: AuthHttpRepository, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
392
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: AuthHttpRepository, providedIn: 'root' });
|
|
393
|
-
}
|
|
394
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: AuthHttpRepository, decorators: [{
|
|
395
|
-
type: Injectable,
|
|
396
|
-
args: [{
|
|
397
|
-
providedIn: 'root',
|
|
398
|
-
}]
|
|
399
|
-
}] });
|
|
400
|
-
|
|
401
|
-
// src/lib/data/repositories/index.ts
|
|
402
|
-
|
|
403
|
-
// src/lib/data/index.ts
|
|
404
|
-
|
|
405
143
|
// src/lib/presentation/stores/auth.store.ts
|
|
406
144
|
class AuthStore {
|
|
407
145
|
authRepository = inject(AuthRepository);
|
|
@@ -507,7 +245,7 @@ class AuthStore {
|
|
|
507
245
|
})
|
|
508
246
|
.pipe(tap(tokens => {
|
|
509
247
|
this.setAuthenticated(tokens);
|
|
510
|
-
}), catchError
|
|
248
|
+
}), catchError(() => {
|
|
511
249
|
this.logout();
|
|
512
250
|
return of(null);
|
|
513
251
|
}), tap({
|
|
@@ -610,8 +348,127 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
610
348
|
}]
|
|
611
349
|
}], ctorParameters: () => [] });
|
|
612
350
|
|
|
613
|
-
|
|
614
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Service to manage URL redirection after authentication
|
|
353
|
+
* Stores the intended URL when session is lost and redirects to it after successful login
|
|
354
|
+
* SSR-compatible by checking platform before accessing sessionStorage
|
|
355
|
+
*/
|
|
356
|
+
class UrlRedirectService {
|
|
357
|
+
REDIRECT_URL_KEY = 'acp_redirect_url';
|
|
358
|
+
EXCLUDED_ROUTES = [
|
|
359
|
+
'/login',
|
|
360
|
+
'/auth',
|
|
361
|
+
'/register',
|
|
362
|
+
'/forgot-password',
|
|
363
|
+
'/reset-password',
|
|
364
|
+
];
|
|
365
|
+
router = inject(Router);
|
|
366
|
+
platformId = inject(PLATFORM_ID);
|
|
367
|
+
document = inject(DOCUMENT);
|
|
368
|
+
/**
|
|
369
|
+
* Stores the current URL for later redirection
|
|
370
|
+
* @param url - The URL to store (defaults to current URL)
|
|
371
|
+
*/
|
|
372
|
+
storeIntendedUrl(url) {
|
|
373
|
+
// Only store in browser environment
|
|
374
|
+
if (!this.isBrowser()) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const urlToStore = url || this.router.url;
|
|
378
|
+
// Don't store authentication-related routes
|
|
379
|
+
if (this.isExcludedRoute(urlToStore)) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
// Don't store URLs with query parameters that might contain sensitive data
|
|
383
|
+
const urlWithoutParams = urlToStore.split('?')[0];
|
|
384
|
+
this.getSessionStorage()?.setItem(this.REDIRECT_URL_KEY, urlWithoutParams);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Gets the stored intended URL
|
|
388
|
+
* @returns The stored URL or null if none exists
|
|
389
|
+
*/
|
|
390
|
+
getIntendedUrl() {
|
|
391
|
+
if (!this.isBrowser()) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return this.getSessionStorage()?.getItem(this.REDIRECT_URL_KEY) || null;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Redirects to the stored URL and clears it from storage
|
|
398
|
+
* @param defaultRoute - The default route to navigate to if no URL is stored
|
|
399
|
+
*/
|
|
400
|
+
redirectToIntendedUrl(defaultRoute = '/') {
|
|
401
|
+
const intendedUrl = this.getIntendedUrl();
|
|
402
|
+
if (intendedUrl && !this.isExcludedRoute(intendedUrl)) {
|
|
403
|
+
this.clearIntendedUrl();
|
|
404
|
+
this.router.navigateByUrl(intendedUrl);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
this.router.navigate([defaultRoute]);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Clears the stored intended URL
|
|
412
|
+
*/
|
|
413
|
+
clearIntendedUrl() {
|
|
414
|
+
if (!this.isBrowser()) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.getSessionStorage()?.removeItem(this.REDIRECT_URL_KEY);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Checks if a URL should be excluded from redirection
|
|
421
|
+
* @param url - The URL to check
|
|
422
|
+
* @returns True if the URL should be excluded
|
|
423
|
+
*/
|
|
424
|
+
isExcludedRoute(url) {
|
|
425
|
+
return this.EXCLUDED_ROUTES.some(route => url.includes(route));
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Stores the current URL if it's not an excluded route
|
|
429
|
+
* Useful for guards and interceptors
|
|
430
|
+
*/
|
|
431
|
+
storeCurrentUrlIfAllowed() {
|
|
432
|
+
const currentUrl = this.router.url;
|
|
433
|
+
if (!this.isExcludedRoute(currentUrl)) {
|
|
434
|
+
this.storeIntendedUrl(currentUrl);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Checks if we're running in a browser environment
|
|
439
|
+
* @returns True if running in browser, false if SSR
|
|
440
|
+
*/
|
|
441
|
+
isBrowser() {
|
|
442
|
+
return isPlatformBrowser(this.platformId);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Safely gets sessionStorage reference
|
|
446
|
+
* @returns sessionStorage object or null if not available
|
|
447
|
+
*/
|
|
448
|
+
getSessionStorage() {
|
|
449
|
+
if (!this.isBrowser()) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
return this.document.defaultView?.sessionStorage || null;
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
// Handle cases where sessionStorage might be disabled
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UrlRedirectService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
461
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UrlRedirectService, providedIn: 'root' });
|
|
462
|
+
}
|
|
463
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UrlRedirectService, decorators: [{
|
|
464
|
+
type: Injectable,
|
|
465
|
+
args: [{
|
|
466
|
+
providedIn: 'root',
|
|
467
|
+
}]
|
|
468
|
+
}] });
|
|
469
|
+
|
|
470
|
+
// src/lib/application/use-cases/login.use-case.ts
|
|
471
|
+
class LoginUseCase extends BaseUseCase {
|
|
615
472
|
authRepository = inject(AuthRepository);
|
|
616
473
|
authStore = inject(AuthStore);
|
|
617
474
|
router = inject(Router);
|
|
@@ -680,7 +537,7 @@ class RefreshTokenUseCase extends BaseUseCase {
|
|
|
680
537
|
const rememberMe = this.tokenRepository.isRememberMeEnabled();
|
|
681
538
|
// Update authentication state
|
|
682
539
|
this.authStore.setAuthenticated(tokens, rememberMe);
|
|
683
|
-
}), catchError
|
|
540
|
+
}), catchError(error => {
|
|
684
541
|
// Don't logout here, let the interceptor handle it
|
|
685
542
|
return throwError(() => error);
|
|
686
543
|
}));
|
|
@@ -730,6 +587,202 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
730
587
|
|
|
731
588
|
// src/lib/application/index.ts
|
|
732
589
|
|
|
590
|
+
// src/lib/data/repositories/auth-http.repository.ts
|
|
591
|
+
function getDeviceInfo() {
|
|
592
|
+
return `${navigator.platform ?? 'Unknown'} - ${navigator.userAgent}`;
|
|
593
|
+
}
|
|
594
|
+
class AuthHttpRepository extends AuthRepository {
|
|
595
|
+
http = inject(HttpClient);
|
|
596
|
+
URL = `${AUTH_API.ACCOUNT}`;
|
|
597
|
+
login(request) {
|
|
598
|
+
return this.http.post(`${this.URL}login`, request, {
|
|
599
|
+
headers: {
|
|
600
|
+
'Device-Info': getDeviceInfo(),
|
|
601
|
+
},
|
|
602
|
+
withCredentials: true,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
register(request) {
|
|
606
|
+
return this.http.post(`${this.URL}register`, request, {
|
|
607
|
+
headers: {
|
|
608
|
+
'Device-Info': getDeviceInfo(),
|
|
609
|
+
},
|
|
610
|
+
withCredentials: true,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
refreshToken(request) {
|
|
614
|
+
return this.http.post(`${this.URL}refresh`, request, {
|
|
615
|
+
headers: {
|
|
616
|
+
'Device-Info': getDeviceInfo(),
|
|
617
|
+
},
|
|
618
|
+
withCredentials: true,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
logout(email, refreshToken) {
|
|
622
|
+
return this.http.post(`${this.URL}logout`, { email, refreshToken: refreshToken || undefined }, {
|
|
623
|
+
headers: {},
|
|
624
|
+
withCredentials: true, // Ensure cookies are sent
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: AuthHttpRepository, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
628
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: AuthHttpRepository, providedIn: 'root' });
|
|
629
|
+
}
|
|
630
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: AuthHttpRepository, decorators: [{
|
|
631
|
+
type: Injectable,
|
|
632
|
+
args: [{
|
|
633
|
+
providedIn: 'root',
|
|
634
|
+
}]
|
|
635
|
+
}] });
|
|
636
|
+
|
|
637
|
+
// src/lib/data/repositories/index.ts
|
|
638
|
+
|
|
639
|
+
// src/lib/data/index.ts
|
|
640
|
+
|
|
641
|
+
// src/lib/domain/models/auth.ts
|
|
642
|
+
|
|
643
|
+
// src/lib/domain/models/index.ts
|
|
644
|
+
|
|
645
|
+
// src/lib/domain/repositories/index.ts
|
|
646
|
+
|
|
647
|
+
// src/lib/domain/index.ts
|
|
648
|
+
|
|
649
|
+
const authGuard = (_route, state) => {
|
|
650
|
+
const tokenRepository = inject(TokenRepository);
|
|
651
|
+
const router = inject(Router);
|
|
652
|
+
const urlRedirectService = inject(UrlRedirectService);
|
|
653
|
+
const environment = inject(ENVIRONMENT);
|
|
654
|
+
if (tokenRepository.isAuthenticated()) {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
// Store the current URL for redirection after login
|
|
658
|
+
urlRedirectService.storeIntendedUrl(state.url);
|
|
659
|
+
// Redirect to login page (configurable via environment)
|
|
660
|
+
router.navigate([environment.loginRoute]);
|
|
661
|
+
return false;
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Interceptor that handles authentication errors and manages URL redirection
|
|
666
|
+
* Captures the current URL when a 401 error occurs and redirects to login
|
|
667
|
+
*/
|
|
668
|
+
const authRedirectInterceptor = (req, next) => {
|
|
669
|
+
const router = inject(Router);
|
|
670
|
+
const urlRedirectService = inject(UrlRedirectService);
|
|
671
|
+
const tokenRepository = inject(TokenRepository);
|
|
672
|
+
const environment = inject(ENVIRONMENT);
|
|
673
|
+
return next(req).pipe(catchError$1((error) => {
|
|
674
|
+
// Handle 401 Unauthorized errors
|
|
675
|
+
if (error.status === 401) {
|
|
676
|
+
// Only store and redirect if user was previously authenticated
|
|
677
|
+
// This prevents redirect loops and handles session expiry scenarios
|
|
678
|
+
if (tokenRepository.isAuthenticated()) {
|
|
679
|
+
// Store the current URL for redirection after re-authentication
|
|
680
|
+
urlRedirectService.storeCurrentUrlIfAllowed();
|
|
681
|
+
// Navigate to login page
|
|
682
|
+
router.navigate([environment.loginRoute]);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// Re-throw the error so other error handlers can process it
|
|
686
|
+
return throwError(() => error);
|
|
687
|
+
}));
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// src/lib/services/csrf.service.ts
|
|
691
|
+
class CsrfService {
|
|
692
|
+
http = inject(HttpClient);
|
|
693
|
+
csrfToken = null;
|
|
694
|
+
/**
|
|
695
|
+
* Get CSRF token, fetching it if not available
|
|
696
|
+
*/
|
|
697
|
+
async getCsrfToken() {
|
|
698
|
+
if (this.csrfToken) {
|
|
699
|
+
return this.csrfToken;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
this.csrfToken = await firstValueFrom(this.http
|
|
703
|
+
.get('/csrf-token')
|
|
704
|
+
.pipe(map(response => response.csrfToken)));
|
|
705
|
+
return this.csrfToken || '';
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
// If CSRF endpoint fails, return empty token
|
|
709
|
+
// Server should handle missing CSRF tokens appropriately
|
|
710
|
+
return '';
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Clear stored CSRF token (useful on logout)
|
|
715
|
+
*/
|
|
716
|
+
clearCsrfToken() {
|
|
717
|
+
this.csrfToken = null;
|
|
718
|
+
}
|
|
719
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: CsrfService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
720
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: CsrfService, providedIn: 'root' });
|
|
721
|
+
}
|
|
722
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: CsrfService, decorators: [{
|
|
723
|
+
type: Injectable,
|
|
724
|
+
args: [{
|
|
725
|
+
providedIn: 'root',
|
|
726
|
+
}]
|
|
727
|
+
}] });
|
|
728
|
+
|
|
729
|
+
// A token to use with HttpContext for skipping CSRF token addition on specific requests.
|
|
730
|
+
const SKIP_CSRF = new HttpContextToken(() => false);
|
|
731
|
+
/**
|
|
732
|
+
* HTTP interceptor that automatically adds CSRF tokens to state-changing requests
|
|
733
|
+
* Only applies to requests to the same origin to avoid leaking tokens to external APIs
|
|
734
|
+
*/
|
|
735
|
+
const csrfInterceptor = (req, next) => {
|
|
736
|
+
const csrfService = inject(CsrfService);
|
|
737
|
+
// Check if CSRF should be skipped for this request
|
|
738
|
+
const skipCsrf = req.context.get(SKIP_CSRF);
|
|
739
|
+
if (skipCsrf) {
|
|
740
|
+
return next(req);
|
|
741
|
+
}
|
|
742
|
+
// Only add CSRF token to state-changing requests (POST, PUT, PATCH, DELETE)
|
|
743
|
+
const isStateChangingMethod = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method.toUpperCase());
|
|
744
|
+
// Only add CSRF token to same-origin requests
|
|
745
|
+
const isSameOrigin = isRequestToSameOrigin(req);
|
|
746
|
+
if (isStateChangingMethod && isSameOrigin) {
|
|
747
|
+
return from(csrfService.getCsrfToken()).pipe(switchMap(csrfToken => {
|
|
748
|
+
const modifiedReq = req.clone({
|
|
749
|
+
setHeaders: {
|
|
750
|
+
'X-CSRF-Token': csrfToken,
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
return next(modifiedReq);
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
// For non-state-changing requests or external requests, proceed without modification
|
|
757
|
+
return next(req);
|
|
758
|
+
};
|
|
759
|
+
/**
|
|
760
|
+
* Checks if the request is going to the same origin as the current application
|
|
761
|
+
*/
|
|
762
|
+
function isRequestToSameOrigin(req) {
|
|
763
|
+
try {
|
|
764
|
+
const requestUrl = new URL(req.url, window.location.origin);
|
|
765
|
+
return requestUrl.origin === window.location.origin;
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
// If URL parsing fails, assume it's not same origin for security
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const authProviders = [
|
|
774
|
+
{
|
|
775
|
+
provide: AuthRepository,
|
|
776
|
+
useClass: AuthHttpRepository,
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
provide: TOKEN_PROVIDER,
|
|
780
|
+
useClass: TokenRepository,
|
|
781
|
+
},
|
|
782
|
+
];
|
|
783
|
+
|
|
784
|
+
// src/lib/providers/index.ts
|
|
785
|
+
|
|
733
786
|
// src/lib/presentation/stores/index.ts
|
|
734
787
|
|
|
735
788
|
// src/lib/presentation/components/login/login.component.ts
|
|
@@ -856,22 +909,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
856
909
|
|
|
857
910
|
// src/lib/presentation/index.ts
|
|
858
911
|
|
|
859
|
-
const authProviders = [
|
|
860
|
-
{
|
|
861
|
-
provide: AuthRepository,
|
|
862
|
-
useClass: AuthHttpRepository,
|
|
863
|
-
},
|
|
864
|
-
{
|
|
865
|
-
provide: TOKEN_PROVIDER,
|
|
866
|
-
useClass: TokenRepository,
|
|
867
|
-
},
|
|
868
|
-
];
|
|
869
|
-
|
|
870
|
-
// src/lib/providers/index.ts
|
|
871
|
-
|
|
872
912
|
/**
|
|
873
913
|
* Generated bundle index. Do not edit.
|
|
874
914
|
*/
|
|
875
915
|
|
|
876
|
-
export { AuthHttpRepository, AuthRepository, AuthStore, LoginComponent, LoginUseCase, LogoutUseCase, RefreshTokenUseCase, RegisterUseCase, TokenRepository, UrlRedirectService, authGuard, authProviders, authRedirectInterceptor };
|
|
916
|
+
export { AuthHttpRepository, AuthRepository, AuthStore, CsrfService, LoginComponent, LoginUseCase, LogoutUseCase, RefreshTokenUseCase, RegisterUseCase, SKIP_CSRF, TokenRepository, UrlRedirectService, authGuard, authProviders, authRedirectInterceptor, csrfInterceptor };
|
|
877
917
|
//# sourceMappingURL=acontplus-ng-auth.mjs.map
|