@acontplus/ng-auth 1.1.2 → 1.1.4
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 +1 -17
- package/fesm2022/acontplus-ng-auth.mjs +389 -291
- package/fesm2022/acontplus-ng-auth.mjs.map +1 -1
- package/index.d.ts +129 -106
- package/package.json +4 -4
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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);
|
|
@@ -127,286 +130,81 @@ class TokenRepository {
|
|
|
127
130
|
const refreshTokenInLocalStorage = localStorage.getItem(this.environment.refreshTokenKey);
|
|
128
131
|
return !!(tokenInLocalStorage || refreshTokenInLocalStorage);
|
|
129
132
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: TokenRepository, decorators: [{
|
|
134
|
-
type: Injectable,
|
|
135
|
-
args: [{
|
|
136
|
-
providedIn: 'root',
|
|
137
|
-
}]
|
|
138
|
-
}] });
|
|
139
|
-
|
|
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()) {
|
|
133
|
+
getUserData() {
|
|
134
|
+
const token = this.getToken();
|
|
135
|
+
if (!token) {
|
|
239
136
|
return null;
|
|
240
137
|
}
|
|
241
138
|
try {
|
|
242
|
-
|
|
139
|
+
const decodedToken = jwtDecode(token);
|
|
140
|
+
const email = decodedToken['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] ??
|
|
141
|
+
decodedToken['email'] ??
|
|
142
|
+
decodedToken['sub'] ??
|
|
143
|
+
decodedToken['user_id'];
|
|
144
|
+
const displayName = decodedToken['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'] ??
|
|
145
|
+
decodedToken['displayName'] ??
|
|
146
|
+
decodedToken['display_name'] ??
|
|
147
|
+
decodedToken['name'] ??
|
|
148
|
+
decodedToken['given_name'];
|
|
149
|
+
const name = decodedToken['name'] ?? displayName;
|
|
150
|
+
if (!email) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const userData = {
|
|
154
|
+
email: email.toString(),
|
|
155
|
+
displayName: displayName?.toString() ?? 'Unknown User',
|
|
156
|
+
name: name?.toString(),
|
|
157
|
+
roles: this.extractArrayField(decodedToken, ['roles', 'role']),
|
|
158
|
+
permissions: this.extractArrayField(decodedToken, ['permissions', 'perms']),
|
|
159
|
+
tenantId: decodedToken['tenantId']?.toString() ??
|
|
160
|
+
decodedToken['tenant_id']?.toString() ??
|
|
161
|
+
decodedToken['tenant']?.toString(),
|
|
162
|
+
companyId: decodedToken['companyId']?.toString() ??
|
|
163
|
+
decodedToken['company_id']?.toString() ??
|
|
164
|
+
decodedToken['organizationId']?.toString() ??
|
|
165
|
+
decodedToken['org_id']?.toString(),
|
|
166
|
+
locale: decodedToken['locale']?.toString(),
|
|
167
|
+
timezone: decodedToken['timezone']?.toString() ?? decodedToken['tz']?.toString(),
|
|
168
|
+
};
|
|
169
|
+
return userData;
|
|
243
170
|
}
|
|
244
171
|
catch {
|
|
245
|
-
// Handle cases where sessionStorage might be disabled
|
|
246
172
|
return null;
|
|
247
173
|
}
|
|
248
174
|
}
|
|
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
175
|
/**
|
|
316
|
-
*
|
|
176
|
+
* Extract array field from decoded token, trying multiple possible field names
|
|
317
177
|
*/
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return '';
|
|
178
|
+
extractArrayField(decodedToken, fieldNames) {
|
|
179
|
+
for (const fieldName of fieldNames) {
|
|
180
|
+
const value = decodedToken[fieldName];
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
return value.map(v => v.toString());
|
|
183
|
+
}
|
|
184
|
+
if (typeof value === 'string') {
|
|
185
|
+
// Handle comma-separated string values
|
|
186
|
+
return value
|
|
187
|
+
.split(',')
|
|
188
|
+
.map(v => v.trim())
|
|
189
|
+
.filter(v => v.length > 0);
|
|
190
|
+
}
|
|
332
191
|
}
|
|
192
|
+
return undefined;
|
|
333
193
|
}
|
|
334
|
-
|
|
335
|
-
|
|
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' });
|
|
194
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: TokenRepository, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
195
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: TokenRepository, providedIn: 'root' });
|
|
393
196
|
}
|
|
394
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type:
|
|
197
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: TokenRepository, decorators: [{
|
|
395
198
|
type: Injectable,
|
|
396
199
|
args: [{
|
|
397
200
|
providedIn: 'root',
|
|
398
201
|
}]
|
|
399
202
|
}] });
|
|
400
203
|
|
|
401
|
-
// src/lib/data/repositories/index.ts
|
|
402
|
-
|
|
403
|
-
// src/lib/data/index.ts
|
|
404
|
-
|
|
405
204
|
// src/lib/presentation/stores/auth.store.ts
|
|
406
205
|
class AuthStore {
|
|
407
206
|
authRepository = inject(AuthRepository);
|
|
408
207
|
tokenRepository = inject(TokenRepository);
|
|
409
|
-
userRepository = inject(UserRepository);
|
|
410
208
|
router = inject(Router);
|
|
411
209
|
ngZone = inject(NgZone);
|
|
412
210
|
// Authentication state signals
|
|
@@ -432,7 +230,7 @@ class AuthStore {
|
|
|
432
230
|
const isAuthenticated = this.tokenRepository.isAuthenticated();
|
|
433
231
|
this._isAuthenticated.set(isAuthenticated);
|
|
434
232
|
if (isAuthenticated) {
|
|
435
|
-
const userData = this.
|
|
233
|
+
const userData = this.tokenRepository.getUserData();
|
|
436
234
|
this._user.set(userData);
|
|
437
235
|
this.scheduleTokenRefresh();
|
|
438
236
|
}
|
|
@@ -494,7 +292,7 @@ class AuthStore {
|
|
|
494
292
|
if (this.refreshInProgress$) {
|
|
495
293
|
return this.refreshInProgress$;
|
|
496
294
|
}
|
|
497
|
-
const userData = this.
|
|
295
|
+
const userData = this.tokenRepository.getUserData();
|
|
498
296
|
const refreshToken = this.tokenRepository.getRefreshToken();
|
|
499
297
|
if (!userData?.email || !refreshToken) {
|
|
500
298
|
this.logout();
|
|
@@ -507,7 +305,7 @@ class AuthStore {
|
|
|
507
305
|
})
|
|
508
306
|
.pipe(tap(tokens => {
|
|
509
307
|
this.setAuthenticated(tokens);
|
|
510
|
-
}), catchError
|
|
308
|
+
}), catchError(() => {
|
|
511
309
|
this.logout();
|
|
512
310
|
return of(null);
|
|
513
311
|
}), tap({
|
|
@@ -526,7 +324,7 @@ class AuthStore {
|
|
|
526
324
|
setAuthenticated(tokens, rememberMe = false) {
|
|
527
325
|
this.tokenRepository.saveTokens(tokens, rememberMe);
|
|
528
326
|
this._isAuthenticated.set(true);
|
|
529
|
-
const userData = this.
|
|
327
|
+
const userData = this.tokenRepository.getUserData();
|
|
530
328
|
this._user.set(userData);
|
|
531
329
|
this.scheduleTokenRefresh();
|
|
532
330
|
}
|
|
@@ -610,13 +408,132 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
610
408
|
}]
|
|
611
409
|
}], ctorParameters: () => [] });
|
|
612
410
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
411
|
+
/**
|
|
412
|
+
* Service to manage URL redirection after authentication
|
|
413
|
+
* Stores the intended URL when session is lost and redirects to it after successful login
|
|
414
|
+
* SSR-compatible by checking platform before accessing sessionStorage
|
|
415
|
+
*/
|
|
416
|
+
class UrlRedirectService {
|
|
417
|
+
REDIRECT_URL_KEY = 'acp_redirect_url';
|
|
418
|
+
EXCLUDED_ROUTES = [
|
|
419
|
+
'/login',
|
|
420
|
+
'/auth',
|
|
421
|
+
'/register',
|
|
422
|
+
'/forgot-password',
|
|
423
|
+
'/reset-password',
|
|
424
|
+
];
|
|
425
|
+
router = inject(Router);
|
|
426
|
+
platformId = inject(PLATFORM_ID);
|
|
427
|
+
document = inject(DOCUMENT);
|
|
428
|
+
/**
|
|
429
|
+
* Stores the current URL for later redirection
|
|
430
|
+
* @param url - The URL to store (defaults to current URL)
|
|
431
|
+
*/
|
|
432
|
+
storeIntendedUrl(url) {
|
|
433
|
+
// Only store in browser environment
|
|
434
|
+
if (!this.isBrowser()) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const urlToStore = url || this.router.url;
|
|
438
|
+
// Don't store authentication-related routes
|
|
439
|
+
if (this.isExcludedRoute(urlToStore)) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// Don't store URLs with query parameters that might contain sensitive data
|
|
443
|
+
const urlWithoutParams = urlToStore.split('?')[0];
|
|
444
|
+
this.getSessionStorage()?.setItem(this.REDIRECT_URL_KEY, urlWithoutParams);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Gets the stored intended URL
|
|
448
|
+
* @returns The stored URL or null if none exists
|
|
449
|
+
*/
|
|
450
|
+
getIntendedUrl() {
|
|
451
|
+
if (!this.isBrowser()) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
return this.getSessionStorage()?.getItem(this.REDIRECT_URL_KEY) || null;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Redirects to the stored URL and clears it from storage
|
|
458
|
+
* @param defaultRoute - The default route to navigate to if no URL is stored
|
|
459
|
+
*/
|
|
460
|
+
redirectToIntendedUrl(defaultRoute = '/') {
|
|
461
|
+
const intendedUrl = this.getIntendedUrl();
|
|
462
|
+
if (intendedUrl && !this.isExcludedRoute(intendedUrl)) {
|
|
463
|
+
this.clearIntendedUrl();
|
|
464
|
+
this.router.navigateByUrl(intendedUrl);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
this.router.navigate([defaultRoute]);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Clears the stored intended URL
|
|
472
|
+
*/
|
|
473
|
+
clearIntendedUrl() {
|
|
474
|
+
if (!this.isBrowser()) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
this.getSessionStorage()?.removeItem(this.REDIRECT_URL_KEY);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Checks if a URL should be excluded from redirection
|
|
481
|
+
* @param url - The URL to check
|
|
482
|
+
* @returns True if the URL should be excluded
|
|
483
|
+
*/
|
|
484
|
+
isExcludedRoute(url) {
|
|
485
|
+
return this.EXCLUDED_ROUTES.some(route => url.includes(route));
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Stores the current URL if it's not an excluded route
|
|
489
|
+
* Useful for guards and interceptors
|
|
490
|
+
*/
|
|
491
|
+
storeCurrentUrlIfAllowed() {
|
|
492
|
+
const currentUrl = this.router.url;
|
|
493
|
+
if (!this.isExcludedRoute(currentUrl)) {
|
|
494
|
+
this.storeIntendedUrl(currentUrl);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Checks if we're running in a browser environment
|
|
499
|
+
* @returns True if running in browser, false if SSR
|
|
500
|
+
*/
|
|
501
|
+
isBrowser() {
|
|
502
|
+
return isPlatformBrowser(this.platformId);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Safely gets sessionStorage reference
|
|
506
|
+
* @returns sessionStorage object or null if not available
|
|
507
|
+
*/
|
|
508
|
+
getSessionStorage() {
|
|
509
|
+
if (!this.isBrowser()) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
return this.document.defaultView?.sessionStorage || null;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// Handle cases where sessionStorage might be disabled
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UrlRedirectService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
521
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UrlRedirectService, providedIn: 'root' });
|
|
522
|
+
}
|
|
523
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UrlRedirectService, decorators: [{
|
|
524
|
+
type: Injectable,
|
|
525
|
+
args: [{
|
|
526
|
+
providedIn: 'root',
|
|
527
|
+
}]
|
|
528
|
+
}] });
|
|
529
|
+
|
|
530
|
+
// src/lib/application/use-cases/login.use-case.ts
|
|
531
|
+
class LoginUseCase extends BaseUseCase {
|
|
532
|
+
authRepository = inject(AuthRepository);
|
|
533
|
+
authStore = inject(AuthStore);
|
|
534
|
+
router = inject(Router);
|
|
535
|
+
urlRedirectService = inject(UrlRedirectService);
|
|
536
|
+
execute(request) {
|
|
620
537
|
return this.authRepository.login(request).pipe(tap(tokens => {
|
|
621
538
|
// Set authentication state with rememberMe preference
|
|
622
539
|
this.authStore.setAuthenticated(tokens, request.rememberMe ?? false);
|
|
@@ -660,11 +577,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
660
577
|
// src/lib/application/use-cases/refresh-token.use-case.ts
|
|
661
578
|
class RefreshTokenUseCase extends BaseUseCase {
|
|
662
579
|
authRepository = inject(AuthRepository);
|
|
663
|
-
userRepository = inject(UserRepository);
|
|
664
580
|
tokenRepository = inject(TokenRepository);
|
|
665
581
|
authStore = inject(AuthStore);
|
|
666
582
|
execute() {
|
|
667
|
-
const userData = this.
|
|
583
|
+
const userData = this.tokenRepository.getUserData();
|
|
668
584
|
const refreshToken = this.tokenRepository.getRefreshToken();
|
|
669
585
|
if (!userData?.email || !refreshToken || refreshToken.trim().length === 0) {
|
|
670
586
|
const error = new Error('No refresh token or email available');
|
|
@@ -680,7 +596,7 @@ class RefreshTokenUseCase extends BaseUseCase {
|
|
|
680
596
|
const rememberMe = this.tokenRepository.isRememberMeEnabled();
|
|
681
597
|
// Update authentication state
|
|
682
598
|
this.authStore.setAuthenticated(tokens, rememberMe);
|
|
683
|
-
}), catchError
|
|
599
|
+
}), catchError(error => {
|
|
684
600
|
// Don't logout here, let the interceptor handle it
|
|
685
601
|
return throwError(() => error);
|
|
686
602
|
}));
|
|
@@ -698,11 +614,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
698
614
|
// src/lib/application/use-cases/logout.use-case.ts
|
|
699
615
|
class LogoutUseCase extends BaseUseCase {
|
|
700
616
|
authRepository = inject(AuthRepository);
|
|
701
|
-
userRepository = inject(UserRepository);
|
|
702
617
|
tokenRepository = inject(TokenRepository);
|
|
703
618
|
authStore = inject(AuthStore);
|
|
704
619
|
execute() {
|
|
705
|
-
const userData = this.
|
|
620
|
+
const userData = this.tokenRepository.getUserData();
|
|
706
621
|
const refreshToken = this.tokenRepository.getRefreshToken();
|
|
707
622
|
if (userData?.email && refreshToken && refreshToken.length > 0) {
|
|
708
623
|
return this.authRepository
|
|
@@ -730,6 +645,202 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
730
645
|
|
|
731
646
|
// src/lib/application/index.ts
|
|
732
647
|
|
|
648
|
+
// src/lib/data/repositories/auth-http.repository.ts
|
|
649
|
+
function getDeviceInfo() {
|
|
650
|
+
return `${navigator.platform ?? 'Unknown'} - ${navigator.userAgent}`;
|
|
651
|
+
}
|
|
652
|
+
class AuthHttpRepository extends AuthRepository {
|
|
653
|
+
http = inject(HttpClient);
|
|
654
|
+
URL = `${AUTH_API.AUTH}`;
|
|
655
|
+
login(request) {
|
|
656
|
+
return this.http.post(`${this.URL}login`, request, {
|
|
657
|
+
headers: {
|
|
658
|
+
'Device-Info': getDeviceInfo(),
|
|
659
|
+
},
|
|
660
|
+
withCredentials: true,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
register(request) {
|
|
664
|
+
return this.http.post(`${this.URL}register`, request, {
|
|
665
|
+
headers: {
|
|
666
|
+
'Device-Info': getDeviceInfo(),
|
|
667
|
+
},
|
|
668
|
+
withCredentials: true,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
refreshToken(request) {
|
|
672
|
+
return this.http.post(`${this.URL}refresh`, request, {
|
|
673
|
+
headers: {
|
|
674
|
+
'Device-Info': getDeviceInfo(),
|
|
675
|
+
},
|
|
676
|
+
withCredentials: true,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
logout(email, refreshToken) {
|
|
680
|
+
return this.http.post(`${this.URL}logout`, { email, refreshToken: refreshToken || undefined }, {
|
|
681
|
+
headers: {},
|
|
682
|
+
withCredentials: true, // Ensure cookies are sent
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: AuthHttpRepository, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
686
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: AuthHttpRepository, providedIn: 'root' });
|
|
687
|
+
}
|
|
688
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: AuthHttpRepository, decorators: [{
|
|
689
|
+
type: Injectable,
|
|
690
|
+
args: [{
|
|
691
|
+
providedIn: 'root',
|
|
692
|
+
}]
|
|
693
|
+
}] });
|
|
694
|
+
|
|
695
|
+
// src/lib/data/repositories/index.ts
|
|
696
|
+
|
|
697
|
+
// src/lib/data/index.ts
|
|
698
|
+
|
|
699
|
+
// src/lib/domain/models/auth.ts
|
|
700
|
+
|
|
701
|
+
// src/lib/domain/models/index.ts
|
|
702
|
+
|
|
703
|
+
// src/lib/domain/repositories/index.ts
|
|
704
|
+
|
|
705
|
+
// src/lib/domain/index.ts
|
|
706
|
+
|
|
707
|
+
const authGuard = (_route, state) => {
|
|
708
|
+
const tokenRepository = inject(TokenRepository);
|
|
709
|
+
const router = inject(Router);
|
|
710
|
+
const urlRedirectService = inject(UrlRedirectService);
|
|
711
|
+
const environment = inject(ENVIRONMENT);
|
|
712
|
+
if (tokenRepository.isAuthenticated()) {
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
// Store the current URL for redirection after login
|
|
716
|
+
urlRedirectService.storeIntendedUrl(state.url);
|
|
717
|
+
// Redirect to login page (configurable via environment)
|
|
718
|
+
router.navigate([`/${environment.loginRoute}`]);
|
|
719
|
+
return false;
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Interceptor that handles authentication errors and manages URL redirection
|
|
724
|
+
* Captures the current URL when a 401 error occurs and redirects to login
|
|
725
|
+
*/
|
|
726
|
+
const authRedirectInterceptor = (req, next) => {
|
|
727
|
+
const router = inject(Router);
|
|
728
|
+
const urlRedirectService = inject(UrlRedirectService);
|
|
729
|
+
const tokenRepository = inject(TokenRepository);
|
|
730
|
+
const environment = inject(ENVIRONMENT);
|
|
731
|
+
return next(req).pipe(catchError$1((error) => {
|
|
732
|
+
// Handle 401 Unauthorized errors
|
|
733
|
+
if (error.status === 401) {
|
|
734
|
+
// Only store and redirect if user was previously authenticated
|
|
735
|
+
// This prevents redirect loops and handles session expiry scenarios
|
|
736
|
+
if (tokenRepository.isAuthenticated()) {
|
|
737
|
+
// Store the current URL for redirection after re-authentication
|
|
738
|
+
urlRedirectService.storeCurrentUrlIfAllowed();
|
|
739
|
+
// Navigate to login page
|
|
740
|
+
router.navigate([`/${environment.loginRoute}`]);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// Re-throw the error so other error handlers can process it
|
|
744
|
+
return throwError(() => error);
|
|
745
|
+
}));
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// src/lib/services/csrf.service.ts
|
|
749
|
+
class CsrfService {
|
|
750
|
+
http = inject(HttpClient);
|
|
751
|
+
csrfToken = null;
|
|
752
|
+
/**
|
|
753
|
+
* Get CSRF token, fetching it if not available
|
|
754
|
+
*/
|
|
755
|
+
async getCsrfToken() {
|
|
756
|
+
if (this.csrfToken) {
|
|
757
|
+
return this.csrfToken;
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
this.csrfToken = await firstValueFrom(this.http
|
|
761
|
+
.get('csrf-token')
|
|
762
|
+
.pipe(map(response => response.csrfToken)));
|
|
763
|
+
return this.csrfToken || '';
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
// If CSRF endpoint fails, return empty token
|
|
767
|
+
// Server should handle missing CSRF tokens appropriately
|
|
768
|
+
return '';
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Clear stored CSRF token (useful on logout)
|
|
773
|
+
*/
|
|
774
|
+
clearCsrfToken() {
|
|
775
|
+
this.csrfToken = null;
|
|
776
|
+
}
|
|
777
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: CsrfService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
778
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: CsrfService, providedIn: 'root' });
|
|
779
|
+
}
|
|
780
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: CsrfService, decorators: [{
|
|
781
|
+
type: Injectable,
|
|
782
|
+
args: [{
|
|
783
|
+
providedIn: 'root',
|
|
784
|
+
}]
|
|
785
|
+
}] });
|
|
786
|
+
|
|
787
|
+
// A token to use with HttpContext for skipping CSRF token addition on specific requests.
|
|
788
|
+
const SKIP_CSRF = new HttpContextToken(() => false);
|
|
789
|
+
/**
|
|
790
|
+
* HTTP interceptor that automatically adds CSRF tokens to state-changing requests
|
|
791
|
+
* Only applies to requests to the same origin to avoid leaking tokens to external APIs
|
|
792
|
+
*/
|
|
793
|
+
const csrfInterceptor = (req, next) => {
|
|
794
|
+
const csrfService = inject(CsrfService);
|
|
795
|
+
// Check if CSRF should be skipped for this request
|
|
796
|
+
const skipCsrf = req.context.get(SKIP_CSRF);
|
|
797
|
+
if (skipCsrf) {
|
|
798
|
+
return next(req);
|
|
799
|
+
}
|
|
800
|
+
// Only add CSRF token to state-changing requests (POST, PUT, PATCH, DELETE)
|
|
801
|
+
const isStateChangingMethod = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method.toUpperCase());
|
|
802
|
+
// Only add CSRF token to same-origin requests
|
|
803
|
+
const isSameOrigin = isRequestToSameOrigin(req);
|
|
804
|
+
if (isStateChangingMethod && isSameOrigin) {
|
|
805
|
+
return from(csrfService.getCsrfToken()).pipe(switchMap(csrfToken => {
|
|
806
|
+
const modifiedReq = req.clone({
|
|
807
|
+
setHeaders: {
|
|
808
|
+
'X-CSRF-Token': csrfToken,
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
return next(modifiedReq);
|
|
812
|
+
}));
|
|
813
|
+
}
|
|
814
|
+
// For non-state-changing requests or external requests, proceed without modification
|
|
815
|
+
return next(req);
|
|
816
|
+
};
|
|
817
|
+
/**
|
|
818
|
+
* Checks if the request is going to the same origin as the current application
|
|
819
|
+
*/
|
|
820
|
+
function isRequestToSameOrigin(req) {
|
|
821
|
+
try {
|
|
822
|
+
const requestUrl = new URL(req.url, window.location.origin);
|
|
823
|
+
return requestUrl.origin === window.location.origin;
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
// If URL parsing fails, assume it's not same origin for security
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const authProviders = [
|
|
832
|
+
{
|
|
833
|
+
provide: AuthRepository,
|
|
834
|
+
useClass: AuthHttpRepository,
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
provide: TOKEN_PROVIDER,
|
|
838
|
+
useClass: TokenRepository,
|
|
839
|
+
},
|
|
840
|
+
];
|
|
841
|
+
|
|
842
|
+
// src/lib/providers/index.ts
|
|
843
|
+
|
|
733
844
|
// src/lib/presentation/stores/index.ts
|
|
734
845
|
|
|
735
846
|
// src/lib/presentation/components/login/login.component.ts
|
|
@@ -856,22 +967,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
856
967
|
|
|
857
968
|
// src/lib/presentation/index.ts
|
|
858
969
|
|
|
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
970
|
/**
|
|
873
971
|
* Generated bundle index. Do not edit.
|
|
874
972
|
*/
|
|
875
973
|
|
|
876
|
-
export { AuthHttpRepository, AuthRepository, AuthStore, LoginComponent, LoginUseCase, LogoutUseCase, RefreshTokenUseCase, RegisterUseCase, TokenRepository, UrlRedirectService, authGuard, authProviders, authRedirectInterceptor };
|
|
974
|
+
export { AuthHttpRepository, AuthRepository, AuthStore, CsrfService, LoginComponent, LoginUseCase, LogoutUseCase, RefreshTokenUseCase, RegisterUseCase, SKIP_CSRF, TokenRepository, UrlRedirectService, authGuard, authProviders, authRedirectInterceptor, csrfInterceptor };
|
|
877
975
|
//# sourceMappingURL=acontplus-ng-auth.mjs.map
|