@hichchi/ngx-auth 0.0.1-alpha.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/fesm2022/hichchi-ngx-auth.mjs +1629 -0
- package/fesm2022/hichchi-ngx-auth.mjs.map +1 -0
- package/index.d.ts +1 -0
- package/lib/auth.module.d.ts +92 -0
- package/lib/components/auth-form/auth-form.component.d.ts +61 -0
- package/lib/components/index.d.ts +1 -0
- package/lib/constants.d.ts +96 -0
- package/lib/directives/index.d.ts +1 -0
- package/lib/directives/permission.directive.d.ts +134 -0
- package/lib/enums/auth-guard-condition.enum.d.ts +38 -0
- package/lib/enums/index.d.ts +1 -0
- package/lib/guards/auth.guard.d.ts +71 -0
- package/lib/guards/index.d.ts +2 -0
- package/lib/guards/role.guard.d.ts +5 -0
- package/lib/index.d.ts +9 -0
- package/lib/interceptors/auth.interceptor.d.ts +101 -0
- package/lib/interceptors/index.d.ts +1 -0
- package/lib/interfaces/auth-config.interface.d.ts +5 -0
- package/lib/interfaces/auth-form-data.interface.d.ts +61 -0
- package/lib/interfaces/auth-guard-option.interface.d.ts +72 -0
- package/lib/interfaces/index.d.ts +3 -0
- package/lib/services/auth.service.d.ts +328 -0
- package/lib/services/index.d.ts +1 -0
- package/lib/state/auth.state.d.ts +155 -0
- package/lib/state/index.d.ts +1 -0
- package/lib/tokens.d.ts +73 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/route.utils.d.ts +100 -0
- package/package.json +42 -0
|
@@ -0,0 +1,1629 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, Inject, computed, inject, input, output, signal, effect, Component, TemplateRef, ViewContainerRef, Directive, NgModule } from '@angular/core';
|
|
3
|
+
import * as i1$1 from '@angular/forms';
|
|
4
|
+
import { Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
5
|
+
import { AuthEndpoint, AuthField, isRoleObject, AuthErrorResponseCode } from '@hichchi/nest-connector/auth';
|
|
6
|
+
import * as i1 from '@angular/common/http';
|
|
7
|
+
import { provideHttpClient } from '@angular/common/http';
|
|
8
|
+
import { take, map, tap, catchError, EMPTY, ReplaySubject, throwError, switchMap, filter } from 'rxjs';
|
|
9
|
+
import { Endpoint, HttpClientErrorStatus } from '@hichchi/nest-connector';
|
|
10
|
+
import { toFirstCase } from '@hichchi/utils';
|
|
11
|
+
import { validatedFormData } from '@hichchi/ngx-utils';
|
|
12
|
+
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
|
|
13
|
+
import { withStorageSync } from '@angular-architects/ngrx-toolkit';
|
|
14
|
+
import { Router } from '@angular/router';
|
|
15
|
+
import * as i3 from '@hichchi/ngx-ui';
|
|
16
|
+
import { ButtonComponent, HcCardComponent, HcSeparatorComponent } from '@hichchi/ngx-ui';
|
|
17
|
+
import { CommonModule } from '@angular/common';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Injection token for authentication configuration
|
|
21
|
+
*
|
|
22
|
+
* This constant defines the injection token used by Angular's dependency injection
|
|
23
|
+
* system to provide authentication configuration throughout the ngx-auth module.
|
|
24
|
+
* It allows the AuthConfig interface to be injected into services and components
|
|
25
|
+
* that need access to authentication settings.
|
|
26
|
+
*
|
|
27
|
+
* The token is used internally by the NgxHichchiAuthModule.forRoot() method to
|
|
28
|
+
* register the authentication configuration as a provider, making it available
|
|
29
|
+
* for injection in services like AuthService.
|
|
30
|
+
*
|
|
31
|
+
* This follows Angular's recommended pattern for providing configuration objects
|
|
32
|
+
* to libraries and modules, ensuring type safety and proper dependency injection.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // Used internally by the module to provide configuration
|
|
37
|
+
* @NgModule({
|
|
38
|
+
* providers: [
|
|
39
|
+
* { provide: AUTH_CONFIG, useValue: config },
|
|
40
|
+
* AuthService
|
|
41
|
+
* ]
|
|
42
|
+
* })
|
|
43
|
+
* export class NgxHichchiAuthModule {
|
|
44
|
+
* static forRoot(config: AuthConfig): ModuleWithProviders<NgxHichchiAuthModule> {
|
|
45
|
+
* return {
|
|
46
|
+
* ngModule: NgxHichchiAuthModule,
|
|
47
|
+
* providers: [
|
|
48
|
+
* { provide: AUTH_CONFIG, useValue: config },
|
|
49
|
+
* provideHttpClient(),
|
|
50
|
+
* AuthService
|
|
51
|
+
* ]
|
|
52
|
+
* };
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* // Injecting the configuration in a service
|
|
60
|
+
* @Injectable()
|
|
61
|
+
* export class AuthService {
|
|
62
|
+
* constructor(
|
|
63
|
+
* @Inject(AUTH_CONFIG) private readonly config: AuthConfig
|
|
64
|
+
* ) {
|
|
65
|
+
* console.log('API Base URL:', this.config.apiBaseURL);
|
|
66
|
+
* console.log('Auth Field:', this.config.authField);
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* // Using in a component (though typically not recommended)
|
|
74
|
+
* @Component({
|
|
75
|
+
* selector: 'app-auth-info',
|
|
76
|
+
* template: `<p>API URL: {{ apiUrl }}</p>`
|
|
77
|
+
* })
|
|
78
|
+
* export class AuthInfoComponent {
|
|
79
|
+
* apiUrl: string;
|
|
80
|
+
*
|
|
81
|
+
* constructor(@Inject(AUTH_CONFIG) private config: AuthConfig) {
|
|
82
|
+
* this.apiUrl = config.apiBaseURL;
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @see {@link AuthConfig} Interface that defines the structure of the configuration object
|
|
88
|
+
* @see {@link NgxHichchiAuthModule} Module that uses this token to provide configuration
|
|
89
|
+
* @see {@link AuthService} Service that injects this configuration
|
|
90
|
+
*/
|
|
91
|
+
const AUTH_CONFIG = "AUTH_CONFIG";
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Width of the Google OAuth authentication popup window in pixels
|
|
95
|
+
*
|
|
96
|
+
* This constant defines the width of the popup window that opens during Google OAuth
|
|
97
|
+
* authentication. The width is optimized to provide a good user experience while
|
|
98
|
+
* ensuring the Google sign-in interface is fully visible and usable.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* // Used internally in AuthService.googleSignIn()
|
|
103
|
+
* const popup = window.open(
|
|
104
|
+
* googleAuthUrl,
|
|
105
|
+
* 'google-login-popup',
|
|
106
|
+
* `width=${GOOGLE_AUTH_POPUP_WIDTH}, height=${GOOGLE_AUTH_POPUP_HEIGHT}`
|
|
107
|
+
* );
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @see {@link AuthService.googleSignIn} Method that uses this constant
|
|
111
|
+
* @see {@link GOOGLE_AUTH_POPUP_HEIGHT} Related constant for popup height
|
|
112
|
+
*/
|
|
113
|
+
const GOOGLE_AUTH_POPUP_WIDTH = 500;
|
|
114
|
+
/**
|
|
115
|
+
* Height of the Google OAuth authentication popup window in pixels
|
|
116
|
+
*
|
|
117
|
+
* This constant defines the height of the popup window that opens during Google OAuth
|
|
118
|
+
* authentication. The height is optimized to accommodate the Google sign-in interface
|
|
119
|
+
* and provide sufficient space for user interaction.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* // Used internally in AuthService.googleSignIn()
|
|
124
|
+
* const popup = window.open(
|
|
125
|
+
* googleAuthUrl,
|
|
126
|
+
* 'google-login-popup',
|
|
127
|
+
* `width=${GOOGLE_AUTH_POPUP_WIDTH}, height=${GOOGLE_AUTH_POPUP_HEIGHT}`
|
|
128
|
+
* );
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @see {@link AuthService.googleSignIn} Method that uses this constant
|
|
132
|
+
* @see {@link GOOGLE_AUTH_POPUP_WIDTH} Related constant for popup width
|
|
133
|
+
*/
|
|
134
|
+
const GOOGLE_AUTH_POPUP_HEIGHT = 600;
|
|
135
|
+
/**
|
|
136
|
+
* Polling interval for checking Google OAuth popup status in milliseconds
|
|
137
|
+
*
|
|
138
|
+
* This constant defines how frequently (in milliseconds) the AuthService checks
|
|
139
|
+
* the status of the Google OAuth popup window. The polling is used to detect
|
|
140
|
+
* when the authentication process is complete or if the user has closed the popup.
|
|
141
|
+
*
|
|
142
|
+
* A shorter interval provides more responsive detection but uses more CPU resources.
|
|
143
|
+
* The current value of 100ms provides a good balance between responsiveness and
|
|
144
|
+
* performance.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* // Used internally in AuthService.googleSignIn()
|
|
149
|
+
* const interval = setInterval(() => {
|
|
150
|
+
* // Check popup status
|
|
151
|
+
* if (popup?.closed) {
|
|
152
|
+
* clearInterval(interval);
|
|
153
|
+
* }
|
|
154
|
+
* // Check for authentication completion
|
|
155
|
+
* }, POPUP_POLLING_INTERVAL_MS);
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @see {@link AuthService.googleSignIn} Method that uses this constant for popup polling
|
|
159
|
+
*/
|
|
160
|
+
const POPUP_POLLING_INTERVAL_MS = 100;
|
|
161
|
+
/**
|
|
162
|
+
* Key used to store authentication guard options in route data
|
|
163
|
+
*
|
|
164
|
+
* This constant defines the property name used to store authentication guard
|
|
165
|
+
* configuration in Angular route data. It allows routes to specify custom
|
|
166
|
+
* authentication requirements and behaviors.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* // In route configuration
|
|
171
|
+
* const routes: Routes = [
|
|
172
|
+
* {
|
|
173
|
+
* path: 'admin',
|
|
174
|
+
* component: AdminComponent,
|
|
175
|
+
* canActivate: [AuthGuard],
|
|
176
|
+
* data: {
|
|
177
|
+
* [AUTH_GUARD_OPTIONS_KEY]: {
|
|
178
|
+
* requiredPermissions: ['admin.read'],
|
|
179
|
+
* redirectTo: '/unauthorized'
|
|
180
|
+
* }
|
|
181
|
+
* }
|
|
182
|
+
* }
|
|
183
|
+
* ];
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* @see {@link AuthGuardOption} Interface defining the structure of guard options
|
|
187
|
+
*/
|
|
188
|
+
const AUTH_GUARD_OPTIONS_KEY = "authGuardOptions";
|
|
189
|
+
|
|
190
|
+
// noinspection JSUnusedGlobalSymbols
|
|
191
|
+
/**
|
|
192
|
+
* Angular authentication service for client-side authentication operations
|
|
193
|
+
*
|
|
194
|
+
* This service provides methods for handling authentication operations in Angular applications,
|
|
195
|
+
* including user sign-in, sign-up, Google OAuth authentication, token management, and sign-out.
|
|
196
|
+
* It communicates with the backend authentication API and handles the client-side aspects
|
|
197
|
+
* of the authentication flow.
|
|
198
|
+
*
|
|
199
|
+
* The service is configured through the AuthConfig interface and automatically handles
|
|
200
|
+
* token expiration date parsing and HTTP request management. It integrates seamlessly
|
|
201
|
+
* with the @hichchi/nest-auth backend module.
|
|
202
|
+
*
|
|
203
|
+
* Key features:
|
|
204
|
+
* - Local authentication (email/username and password)
|
|
205
|
+
* - Google OAuth authentication with popup flow
|
|
206
|
+
* - Token refresh functionality
|
|
207
|
+
* - User registration
|
|
208
|
+
* - Automatic token expiration handling
|
|
209
|
+
* - RESTful API communication
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* // In a component
|
|
214
|
+
* export class LoginComponent {
|
|
215
|
+
* constructor(private authService: AuthService) {}
|
|
216
|
+
*
|
|
217
|
+
* async signIn() {
|
|
218
|
+
* try {
|
|
219
|
+
* const response = await this.authService.signIn({
|
|
220
|
+
* email: 'user@example.com',
|
|
221
|
+
* password: 'password123'
|
|
222
|
+
* }).toPromise();
|
|
223
|
+
* console.log('Signed in:', response.user);
|
|
224
|
+
* } catch (error) {
|
|
225
|
+
* console.error('Sign in failed:', error);
|
|
226
|
+
* }
|
|
227
|
+
* }
|
|
228
|
+
* }
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @see {@link AuthConfig} Configuration interface for the authentication service
|
|
232
|
+
* @see {@link NgxHichchiAuthModule} Module that provides this service
|
|
233
|
+
* @see {@link AuthState} State management service for authentication
|
|
234
|
+
* @see {@link AuthResponse} Response interface for authentication operations
|
|
235
|
+
*/
|
|
236
|
+
class AuthService {
|
|
237
|
+
http;
|
|
238
|
+
config;
|
|
239
|
+
/**
|
|
240
|
+
* Creates an instance of AuthService
|
|
241
|
+
*
|
|
242
|
+
* @param http - Http client
|
|
243
|
+
* @param config - The authentication configuration injected from AUTH_CONFIG token
|
|
244
|
+
*
|
|
245
|
+
* @see {@link AUTH_CONFIG} Injection token for authentication configuration
|
|
246
|
+
* @see {@link AuthConfig} Interface defining the configuration structure
|
|
247
|
+
*/
|
|
248
|
+
constructor(http, config) {
|
|
249
|
+
this.http = http;
|
|
250
|
+
this.config = config;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Authenticates a user with email/username and password
|
|
254
|
+
*
|
|
255
|
+
* This method sends a sign-in request to the backend authentication API with the provided
|
|
256
|
+
* credentials. It automatically converts the token expiration timestamps from the response
|
|
257
|
+
* into JavaScript Date objects for easier handling in the client application.
|
|
258
|
+
*
|
|
259
|
+
* @param dto - The sign-in data containing user credentials
|
|
260
|
+
* @returns Observable that emits the authentication response with user data and tokens
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* // Sign in with email and password
|
|
265
|
+
* this.authService.signIn({
|
|
266
|
+
* email: 'user@example.com',
|
|
267
|
+
* password: 'password123'
|
|
268
|
+
* }).subscribe({
|
|
269
|
+
* next: (response) => {
|
|
270
|
+
* console.log('User signed in:', response.user);
|
|
271
|
+
* console.log('Access token expires:', response.accessTokenExpiresOn);
|
|
272
|
+
* },
|
|
273
|
+
* error: (error) => {
|
|
274
|
+
* console.error('Sign in failed:', error);
|
|
275
|
+
* }
|
|
276
|
+
* });
|
|
277
|
+
* ```
|
|
278
|
+
*
|
|
279
|
+
* @see {@link SignInBody} Interface for sign-in request data
|
|
280
|
+
* @see {@link AuthResponse} Interface for authentication response
|
|
281
|
+
* @see {@link AuthEndpoint.SIGN_IN} Backend endpoint for user authentication
|
|
282
|
+
*/
|
|
283
|
+
signIn(dto) {
|
|
284
|
+
return this.http.post(`${Endpoint.AUTH}/${AuthEndpoint.SIGN_IN}`, dto).pipe(take(1), map(res => ({
|
|
285
|
+
...res,
|
|
286
|
+
accessTokenExpiresOn: new Date(res.accessTokenExpiresOn),
|
|
287
|
+
refreshTokenExpiresOn: new Date(res.refreshTokenExpiresOn),
|
|
288
|
+
})));
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Initiates Google OAuth authentication using a popup window
|
|
292
|
+
*
|
|
293
|
+
* This method opens a popup window that navigates to the Google OAuth authentication
|
|
294
|
+
* endpoint. It handles the OAuth flow by monitoring the popup window and extracting
|
|
295
|
+
* the access token from the callback URL when authentication is successful.
|
|
296
|
+
*
|
|
297
|
+
* The popup is automatically positioned in the center of the screen and has predefined
|
|
298
|
+
* dimensions for optimal user experience. The method polls the popup window to detect
|
|
299
|
+
* when authentication is complete or if the user closes the popup.
|
|
300
|
+
*
|
|
301
|
+
* @returns Promise that resolves with the access token when authentication succeeds
|
|
302
|
+
*
|
|
303
|
+
* @throws {Error} If authentication fails or the popup is blocked
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```typescript
|
|
307
|
+
* // Initiate Google sign-in
|
|
308
|
+
* try {
|
|
309
|
+
* const accessToken = await this.authService.googleSignIn();
|
|
310
|
+
* console.log('Google authentication successful:', accessToken);
|
|
311
|
+
*
|
|
312
|
+
* // Use the token to get full auth response
|
|
313
|
+
* const authResponse = await this.authService.getAuthResponse(accessToken).toPromise();
|
|
314
|
+
* console.log('User data:', authResponse.user);
|
|
315
|
+
* } catch (error) {
|
|
316
|
+
* console.error('Google authentication failed:', error);
|
|
317
|
+
* }
|
|
318
|
+
* ```
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```typescript
|
|
322
|
+
* // In a component with error handling
|
|
323
|
+
* async signInWithGoogle() {
|
|
324
|
+
* try {
|
|
325
|
+
* const token = await this.authService.googleSignIn();
|
|
326
|
+
* // Handle successful authentication
|
|
327
|
+
* this.router.navigate(['/dashboard']);
|
|
328
|
+
* } catch (error) {
|
|
329
|
+
* if (error.message.includes('popup')) {
|
|
330
|
+
* this.showError('Please allow popups for Google sign-in');
|
|
331
|
+
* } else {
|
|
332
|
+
* this.showError('Google sign-in failed. Please try again.');
|
|
333
|
+
* }
|
|
334
|
+
* }
|
|
335
|
+
* }
|
|
336
|
+
* ```
|
|
337
|
+
*
|
|
338
|
+
* @see {@link getAuthResponse} Method to get full authentication response using the access token
|
|
339
|
+
* @see {@link AuthEndpoint.GOOGLE_SIGN_IN} Backend endpoint for Google OAuth initiation
|
|
340
|
+
* @see {@link GOOGLE_AUTH_POPUP_WIDTH} Constant defining popup window width
|
|
341
|
+
* @see {@link GOOGLE_AUTH_POPUP_HEIGHT} Constant defining popup window height
|
|
342
|
+
* @see {@link POPUP_POLLING_INTERVAL_MS} Constant defining popup polling interval
|
|
343
|
+
*/
|
|
344
|
+
googleSignIn() {
|
|
345
|
+
return new Promise((resolve, reject) => {
|
|
346
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
347
|
+
const left = (window.screen.width - GOOGLE_AUTH_POPUP_WIDTH) / 2;
|
|
348
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
349
|
+
const top = (window.screen.height - GOOGLE_AUTH_POPUP_HEIGHT) / 2;
|
|
350
|
+
const popup = window.open(`${this.config.apiBaseURL}/${Endpoint.AUTH}/${AuthEndpoint.GOOGLE_SIGN_IN}?redirectUrl=${window.location.origin}`, "google-login-popup",
|
|
351
|
+
// eslint-disable-next-line prefer-template
|
|
352
|
+
"resizable=no, location=no, toolbar=false, width=" +
|
|
353
|
+
GOOGLE_AUTH_POPUP_WIDTH +
|
|
354
|
+
", height=" +
|
|
355
|
+
GOOGLE_AUTH_POPUP_HEIGHT +
|
|
356
|
+
", top=" +
|
|
357
|
+
top +
|
|
358
|
+
", left=" +
|
|
359
|
+
left);
|
|
360
|
+
const interval = setInterval(() => {
|
|
361
|
+
if (popup?.closed) {
|
|
362
|
+
clearInterval(interval);
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
if (popup?.location.href !== "about:blank" && popup?.location?.search?.includes("?token=e")) {
|
|
366
|
+
const token = popup.location.search.split("=")[1];
|
|
367
|
+
clearInterval(interval);
|
|
368
|
+
popup.close();
|
|
369
|
+
resolve(token);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
if (!String(error).includes("SecurityError")) {
|
|
374
|
+
clearInterval(interval);
|
|
375
|
+
reject(error);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}, POPUP_POLLING_INTERVAL_MS);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Retrieves the complete authentication response using an access token
|
|
383
|
+
*
|
|
384
|
+
* This method exchanges an access token for a complete authentication response
|
|
385
|
+
* containing user information and token details. It's typically used after
|
|
386
|
+
* Google OAuth authentication to get the full user profile and session data.
|
|
387
|
+
*
|
|
388
|
+
* The method automatically converts token expiration timestamps to JavaScript
|
|
389
|
+
* Date objects for easier handling in the client application.
|
|
390
|
+
*
|
|
391
|
+
* @param accessToken - The access token to exchange for authentication response
|
|
392
|
+
* @returns Observable that emits the complete authentication response
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```typescript
|
|
396
|
+
* // Get auth response after Google sign-in
|
|
397
|
+
* const accessToken = await this.authService.googleSignIn();
|
|
398
|
+
* this.authService.getAuthResponse(accessToken).subscribe({
|
|
399
|
+
* next: (response) => {
|
|
400
|
+
* console.log('User:', response.user);
|
|
401
|
+
* console.log('Tokens:', {
|
|
402
|
+
* access: response.accessToken,
|
|
403
|
+
* refresh: response.refreshToken
|
|
404
|
+
* });
|
|
405
|
+
* },
|
|
406
|
+
* error: (error) => {
|
|
407
|
+
* console.error('Failed to get auth response:', error);
|
|
408
|
+
* }
|
|
409
|
+
* });
|
|
410
|
+
* ```
|
|
411
|
+
*
|
|
412
|
+
* @see {@link AccessToken} Type representing access tokens
|
|
413
|
+
* @see {@link AuthResponse} Interface for complete authentication response
|
|
414
|
+
* @see {@link AuthEndpoint.GET_AUTH_RESPONSE} Backend endpoint for token exchange
|
|
415
|
+
* @see {@link googleSignIn} Method that provides access tokens for this operation
|
|
416
|
+
*/
|
|
417
|
+
getAuthResponse(accessToken) {
|
|
418
|
+
return this.http
|
|
419
|
+
.post(`${Endpoint.AUTH}/${AuthEndpoint.GET_AUTH_RESPONSE}`, {
|
|
420
|
+
accessToken,
|
|
421
|
+
})
|
|
422
|
+
.pipe(take(1), map(res => ({
|
|
423
|
+
...res,
|
|
424
|
+
accessTokenExpiresOn: new Date(res.accessTokenExpiresOn),
|
|
425
|
+
refreshTokenExpiresOn: new Date(res.refreshTokenExpiresOn),
|
|
426
|
+
})));
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Registers a new user account
|
|
430
|
+
*
|
|
431
|
+
* This method sends a registration request to the backend API with the provided
|
|
432
|
+
* user information. It creates a new user account and returns the user data
|
|
433
|
+
* upon successful registration.
|
|
434
|
+
*
|
|
435
|
+
* Note that this method only creates the user account and does not automatically
|
|
436
|
+
* sign the user in. After successful registration, you may need to call signIn
|
|
437
|
+
* or handle email verification depending on your application's configuration.
|
|
438
|
+
*
|
|
439
|
+
* @param dto - The sign-up data containing user registration information
|
|
440
|
+
* @returns Observable that emits the newly created user data
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```typescript
|
|
444
|
+
* // Register a new user
|
|
445
|
+
* this.authService.signUp({
|
|
446
|
+
* email: 'newuser@example.com',
|
|
447
|
+
* password: 'securePassword123',
|
|
448
|
+
* firstName: 'John',
|
|
449
|
+
* lastName: 'Doe'
|
|
450
|
+
* }).subscribe({
|
|
451
|
+
* next: (user) => {
|
|
452
|
+
* console.log('User registered successfully:', user);
|
|
453
|
+
* // Optionally redirect to sign-in or email verification page
|
|
454
|
+
* this.router.navigate(['/verify-email']);
|
|
455
|
+
* },
|
|
456
|
+
* error: (error) => {
|
|
457
|
+
* console.error('Registration failed:', error);
|
|
458
|
+
* // Handle registration errors (email already exists, etc.)
|
|
459
|
+
* }
|
|
460
|
+
* });
|
|
461
|
+
* ```
|
|
462
|
+
*
|
|
463
|
+
* @see {@link SignUpBody} Interface for user registration data
|
|
464
|
+
* @see {@link User} Interface for user data returned after registration
|
|
465
|
+
* @see {@link AuthEndpoint.SIGN_UP} Backend endpoint for user registration
|
|
466
|
+
* @see {@link signIn} Method to authenticate user after registration
|
|
467
|
+
*/
|
|
468
|
+
signUp(dto) {
|
|
469
|
+
return this.http.post(`${Endpoint.AUTH}/${AuthEndpoint.SIGN_UP}`, dto).pipe(take(1));
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Refreshes an expired access token using a refresh token
|
|
473
|
+
*
|
|
474
|
+
* This method exchanges a valid refresh token for a new set of access and refresh tokens.
|
|
475
|
+
* It's typically used when the current access token has expired but the refresh token
|
|
476
|
+
* is still valid, allowing the user to maintain their session without re-authenticating.
|
|
477
|
+
*
|
|
478
|
+
* The refresh token mechanism provides a secure way to maintain long-lived sessions
|
|
479
|
+
* while keeping access tokens short-lived for better security.
|
|
480
|
+
*
|
|
481
|
+
* @param refreshToken - The refresh token to exchange for new tokens
|
|
482
|
+
* @returns Observable that emits the new token response
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* ```typescript
|
|
486
|
+
* // Refresh tokens when access token expires
|
|
487
|
+
* const storedRefreshToken = localStorage.getItem('refreshToken');
|
|
488
|
+
* if (storedRefreshToken) {
|
|
489
|
+
* this.authService.refreshToken(storedRefreshToken).subscribe({
|
|
490
|
+
* next: (tokenResponse) => {
|
|
491
|
+
* console.log('Tokens refreshed successfully');
|
|
492
|
+
* // Store new tokens
|
|
493
|
+
* localStorage.setItem('accessToken', tokenResponse.accessToken);
|
|
494
|
+
* localStorage.setItem('refreshToken', tokenResponse.refreshToken);
|
|
495
|
+
* },
|
|
496
|
+
* error: (error) => {
|
|
497
|
+
* console.error('Token refresh failed:', error);
|
|
498
|
+
* // Redirect to login page
|
|
499
|
+
* this.router.navigate(['/login']);
|
|
500
|
+
* }
|
|
501
|
+
* });
|
|
502
|
+
* }
|
|
503
|
+
* ```
|
|
504
|
+
*
|
|
505
|
+
* @see {@link RefreshToken} Type representing refresh tokens
|
|
506
|
+
* @see {@link TokenResponse} Interface for token refresh response
|
|
507
|
+
* @see {@link AuthEndpoint.REFRESH_TOKEN} Backend endpoint for token refresh
|
|
508
|
+
* @see {@link signIn} Method to get initial tokens through authentication
|
|
509
|
+
*/
|
|
510
|
+
refreshToken(refreshToken) {
|
|
511
|
+
return this.http
|
|
512
|
+
.post(`${Endpoint.AUTH}/${AuthEndpoint.REFRESH_TOKEN}`, {
|
|
513
|
+
refreshToken,
|
|
514
|
+
})
|
|
515
|
+
.pipe(take(1));
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Signs out the current user and invalidates their session
|
|
519
|
+
*
|
|
520
|
+
* This method sends a sign-out request to the backend API to invalidate the user's
|
|
521
|
+
* current session and tokens. It effectively logs the user out of the application
|
|
522
|
+
* and clears their authentication state on the server.
|
|
523
|
+
*
|
|
524
|
+
* After calling this method, you should also clear any client-side authentication
|
|
525
|
+
* data such as tokens stored in localStorage, sessionStorage, or application state.
|
|
526
|
+
*
|
|
527
|
+
* @returns Observable that emits a success response when sign-out is complete
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```typescript
|
|
531
|
+
* // Sign out the current user
|
|
532
|
+
* this.authService.signOut().subscribe({
|
|
533
|
+
* next: (response) => {
|
|
534
|
+
* console.log('User signed out successfully');
|
|
535
|
+
* // Clear client-side authentication data
|
|
536
|
+
* localStorage.removeItem('accessToken');
|
|
537
|
+
* localStorage.removeItem('refreshToken');
|
|
538
|
+
* // Redirect to login page
|
|
539
|
+
* this.router.navigate(['/login']);
|
|
540
|
+
* },
|
|
541
|
+
* error: (error) => {
|
|
542
|
+
* console.error('Sign out failed:', error);
|
|
543
|
+
* // Even if server sign-out fails, clear local data
|
|
544
|
+
* localStorage.clear();
|
|
545
|
+
* this.router.navigate(['/login']);
|
|
546
|
+
* }
|
|
547
|
+
* });
|
|
548
|
+
* ```
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* ```typescript
|
|
552
|
+
* // Sign out with state management
|
|
553
|
+
* async signOut() {
|
|
554
|
+
* try {
|
|
555
|
+
* await this.authService.signOut().toPromise();
|
|
556
|
+
* // Clear authentication state
|
|
557
|
+
* this.authState.clearUser();
|
|
558
|
+
* this.notificationService.showSuccess('Signed out successfully');
|
|
559
|
+
* } catch (error) {
|
|
560
|
+
* console.error('Sign out error:', error);
|
|
561
|
+
* } finally {
|
|
562
|
+
* // Always redirect to login
|
|
563
|
+
* this.router.navigate(['/login']);
|
|
564
|
+
* }
|
|
565
|
+
* }
|
|
566
|
+
* ```
|
|
567
|
+
*
|
|
568
|
+
* @see {@link SuccessResponse} Interface for success response
|
|
569
|
+
* @see {@link AuthEndpoint.SIGN_OUT} Backend endpoint for user sign-out
|
|
570
|
+
* @see {@link signIn} Method to authenticate user after sign-out
|
|
571
|
+
*/
|
|
572
|
+
signOut() {
|
|
573
|
+
// this.app.startSpinner();
|
|
574
|
+
return this.http.post(`${Endpoint.AUTH}/${AuthEndpoint.SIGN_OUT}`, {}).pipe(take(1));
|
|
575
|
+
}
|
|
576
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AuthService, deps: [{ token: i1.HttpClient }, { token: AUTH_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
577
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AuthService, providedIn: "root" });
|
|
578
|
+
}
|
|
579
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AuthService, decorators: [{
|
|
580
|
+
type: Injectable,
|
|
581
|
+
args: [{
|
|
582
|
+
providedIn: "root",
|
|
583
|
+
}]
|
|
584
|
+
}], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
|
|
585
|
+
type: Inject,
|
|
586
|
+
args: [AUTH_CONFIG]
|
|
587
|
+
}] }] });
|
|
588
|
+
|
|
589
|
+
/* eslint-disable */
|
|
590
|
+
// noinspection JSUnusedGlobalSymbols
|
|
591
|
+
const initialState = {
|
|
592
|
+
signedIn: false,
|
|
593
|
+
sessionId: null,
|
|
594
|
+
user: null,
|
|
595
|
+
accessToken: null,
|
|
596
|
+
refreshToken: null,
|
|
597
|
+
accessTokenExpiresOn: null,
|
|
598
|
+
refreshTokenExpiresOn: null,
|
|
599
|
+
};
|
|
600
|
+
/**
|
|
601
|
+
* Authentication state management store using NgRx Signals
|
|
602
|
+
*
|
|
603
|
+
* This signal store provides centralized state management for authentication in Angular applications.
|
|
604
|
+
* It manages user authentication state, tokens, and provides methods for authentication operations.
|
|
605
|
+
* The store automatically persists state to browser storage and provides reactive computed values.
|
|
606
|
+
*
|
|
607
|
+
* Key features:
|
|
608
|
+
* - Automatic state persistence with browser storage sync
|
|
609
|
+
* - Reactive computed properties for common authentication checks
|
|
610
|
+
* - Built-in methods for sign-in, sign-out, and token management
|
|
611
|
+
* - Integration with Angular Router for navigation after authentication
|
|
612
|
+
* - Type-safe state management with TypeScript
|
|
613
|
+
*
|
|
614
|
+
* The store is provided at the root level and can be injected into any component or service.
|
|
615
|
+
* It uses NgRx Signals for reactive state management and provides a modern alternative
|
|
616
|
+
* to traditional NgRx store patterns.
|
|
617
|
+
*
|
|
618
|
+
* @example
|
|
619
|
+
* ```typescript
|
|
620
|
+
* // In a component
|
|
621
|
+
* export class AppComponent {
|
|
622
|
+
* private authState = inject(AuthState)
|
|
623
|
+
*
|
|
624
|
+
* // Access reactive state
|
|
625
|
+
* isSignedIn = this.authState.signedIn;
|
|
626
|
+
* currentUser = this.authState.user;
|
|
627
|
+
* hasAccessToken = this.authState.hasAccessToken;
|
|
628
|
+
*
|
|
629
|
+
* // Sign in user
|
|
630
|
+
* async signIn() {
|
|
631
|
+
* this.authState.signIn({
|
|
632
|
+
* email: 'user@example.com',
|
|
633
|
+
* password: 'password123'
|
|
634
|
+
* }, '/dashboard').subscribe({
|
|
635
|
+
* next: (response) => console.log('Signed in:', response.user),
|
|
636
|
+
* error: (error) => console.error('Sign in failed:', error)
|
|
637
|
+
* });
|
|
638
|
+
* }
|
|
639
|
+
*
|
|
640
|
+
* // Sign out user
|
|
641
|
+
* signOut() {
|
|
642
|
+
* this.authState.signOut('/login').subscribe();
|
|
643
|
+
* }
|
|
644
|
+
* }
|
|
645
|
+
* ```
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```typescript
|
|
649
|
+
* // In a guard
|
|
650
|
+
* export class AuthGuard {
|
|
651
|
+
* private authState = inject(AuthState)
|
|
652
|
+
*
|
|
653
|
+
* canActivate(): boolean {
|
|
654
|
+
* return this.authState.signedIn() && this.authState.hasAccessToken();
|
|
655
|
+
* }
|
|
656
|
+
* }
|
|
657
|
+
* ```
|
|
658
|
+
*
|
|
659
|
+
* @example
|
|
660
|
+
* ```typescript
|
|
661
|
+
* // Using computed properties
|
|
662
|
+
* export class HeaderComponent {
|
|
663
|
+
* private authState = inject(AuthState)
|
|
664
|
+
*
|
|
665
|
+
* // Reactive computed values
|
|
666
|
+
* userRole = this.authState.role;
|
|
667
|
+
* isEmailVerified = this.authState.emailVerified;
|
|
668
|
+
* hasValidToken = this.authState.hasAccessToken;
|
|
669
|
+
* }
|
|
670
|
+
* ```
|
|
671
|
+
*
|
|
672
|
+
* @see {@link AuthStateModel} Interface defining the state structure
|
|
673
|
+
* @see {@link AuthService} Service used for authentication operations
|
|
674
|
+
* @see {@link signalStore} NgRx Signals store factory function
|
|
675
|
+
* @see {@link withStorageSync} Storage synchronization feature
|
|
676
|
+
*/
|
|
677
|
+
const AuthState = signalStore({ providedIn: "root" }, withState(initialState), withStorageSync({ key: "auth" }), withComputed(({ accessToken, user }) => ({
|
|
678
|
+
hasAccessToken: computed(() => Boolean(accessToken())),
|
|
679
|
+
role: computed(() => user()?.role),
|
|
680
|
+
emailVerified: computed(() => Boolean(user()?.emailVerified)),
|
|
681
|
+
})), withMethods((store, router = inject(Router), authService = inject(AuthService)) => ({
|
|
682
|
+
reset() {
|
|
683
|
+
patchState(store, initialState);
|
|
684
|
+
},
|
|
685
|
+
setTokens(tokenResponse) {
|
|
686
|
+
const { accessToken, refreshToken, accessTokenExpiresOn, refreshTokenExpiresOn } = tokenResponse;
|
|
687
|
+
patchState(store, state => ({
|
|
688
|
+
...state,
|
|
689
|
+
accessToken,
|
|
690
|
+
refreshToken,
|
|
691
|
+
accessTokenExpiresOn,
|
|
692
|
+
refreshTokenExpiresOn,
|
|
693
|
+
}));
|
|
694
|
+
},
|
|
695
|
+
signIn(signInBody, redirect) {
|
|
696
|
+
return authService.signIn(signInBody).pipe(tap((res) => {
|
|
697
|
+
patchState(store, { ...res, signedIn: true });
|
|
698
|
+
if (redirect) {
|
|
699
|
+
void router.navigateByUrl(typeof redirect === "string" ? redirect : redirect(res));
|
|
700
|
+
}
|
|
701
|
+
}));
|
|
702
|
+
},
|
|
703
|
+
authenticateWithToken: (accessToken, redirect) => {
|
|
704
|
+
return authService.getAuthResponse(accessToken).pipe(tap((res) => {
|
|
705
|
+
patchState(store, { ...res, signedIn: Boolean(res.user.role) });
|
|
706
|
+
if (redirect) {
|
|
707
|
+
void router.navigateByUrl(typeof redirect === "string" ? redirect : redirect(res));
|
|
708
|
+
}
|
|
709
|
+
}), catchError(() => EMPTY));
|
|
710
|
+
},
|
|
711
|
+
signOut: (redirect) => {
|
|
712
|
+
return authService.signOut().pipe(tap({
|
|
713
|
+
next: () => {
|
|
714
|
+
patchState(store, initialState);
|
|
715
|
+
if (redirect) {
|
|
716
|
+
void router.navigateByUrl(redirect);
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
}), catchError(() => EMPTY));
|
|
720
|
+
},
|
|
721
|
+
})));
|
|
722
|
+
|
|
723
|
+
/* eslint-disable @angular-eslint/no-output-on-prefix */
|
|
724
|
+
class AuthFormComponent {
|
|
725
|
+
config;
|
|
726
|
+
fb;
|
|
727
|
+
authService;
|
|
728
|
+
local = input(true);
|
|
729
|
+
google = input(true);
|
|
730
|
+
facebook = input(true);
|
|
731
|
+
onError = output();
|
|
732
|
+
onSignIn = output();
|
|
733
|
+
onSignUp = output();
|
|
734
|
+
isLoading = signal(false);
|
|
735
|
+
isSignUp = signal(false);
|
|
736
|
+
isError = signal(false);
|
|
737
|
+
authField = signal(AuthField.EMAIL);
|
|
738
|
+
authFieldLabel = signal(toFirstCase(AuthField.EMAIL));
|
|
739
|
+
error = signal(null);
|
|
740
|
+
authState = inject(AuthState);
|
|
741
|
+
authForm;
|
|
742
|
+
constructor(config, fb, authService) {
|
|
743
|
+
this.config = config;
|
|
744
|
+
this.fb = fb;
|
|
745
|
+
this.authService = authService;
|
|
746
|
+
this.authField.set(config.authField === AuthField.USERNAME ? AuthField.USERNAME : AuthField.EMAIL);
|
|
747
|
+
this.authFieldLabel.set(toFirstCase(this.authField()));
|
|
748
|
+
this.authForm = this.fb.group({
|
|
749
|
+
firstName: ["", Validators.required],
|
|
750
|
+
lastName: ["", Validators.required],
|
|
751
|
+
authFieldValue: ["", [Validators.required]],
|
|
752
|
+
password: ["", Validators.required],
|
|
753
|
+
});
|
|
754
|
+
effect(() => {
|
|
755
|
+
if (this.isSignUp()) {
|
|
756
|
+
this.authForm.controls?.firstName?.enable();
|
|
757
|
+
this.authForm.controls?.lastName?.enable();
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
this.authForm.controls?.firstName?.disable();
|
|
761
|
+
this.authForm.controls?.lastName?.disable();
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
async handleGoogleSignIn() {
|
|
766
|
+
const accessToken = await this.authService.googleSignIn();
|
|
767
|
+
this.authState.authenticateWithToken(accessToken).subscribe({
|
|
768
|
+
next: authResponse => {
|
|
769
|
+
this.isLoading.set(false);
|
|
770
|
+
this.onSignIn.emit(authResponse);
|
|
771
|
+
},
|
|
772
|
+
error: this.handleError.bind(this),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
handleLocalAuth(signInBody) {
|
|
776
|
+
this.isLoading.set(true);
|
|
777
|
+
this.isError.set(false);
|
|
778
|
+
this.authState.signIn(signInBody).subscribe({
|
|
779
|
+
next: authResponse => {
|
|
780
|
+
this.isLoading.set(false);
|
|
781
|
+
this.onSignIn.emit(authResponse);
|
|
782
|
+
},
|
|
783
|
+
error: this.handleError.bind(this),
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
handleSignUp(signUpBody) {
|
|
787
|
+
this.isLoading.set(true);
|
|
788
|
+
this.isError.set(false);
|
|
789
|
+
this.authService.signUp(signUpBody).subscribe({
|
|
790
|
+
next: user => {
|
|
791
|
+
this.isLoading.set(false);
|
|
792
|
+
this.onSignUp.emit(user);
|
|
793
|
+
},
|
|
794
|
+
error: this.handleError.bind(this),
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
handleSubmit(e) {
|
|
798
|
+
e.preventDefault();
|
|
799
|
+
if (this.isSignUp()) {
|
|
800
|
+
const formData = validatedFormData(this.authForm);
|
|
801
|
+
if (formData) {
|
|
802
|
+
this.handleSignUp({
|
|
803
|
+
firstName: formData.firstName,
|
|
804
|
+
lastName: formData.lastName,
|
|
805
|
+
[this.authField()]: formData.authFieldValue,
|
|
806
|
+
password: formData.password,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
const formData = validatedFormData(this.authForm);
|
|
812
|
+
if (formData) {
|
|
813
|
+
this.handleLocalAuth({
|
|
814
|
+
[this.authField()]: formData.authFieldValue,
|
|
815
|
+
password: formData.password,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
handleError(error) {
|
|
821
|
+
this.isLoading.set(false);
|
|
822
|
+
this.isError.set(true);
|
|
823
|
+
this.error.set(error);
|
|
824
|
+
this.onError.emit(error);
|
|
825
|
+
}
|
|
826
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AuthFormComponent, deps: [{ token: AUTH_CONFIG }, { token: i1$1.FormBuilder }, { token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
|
|
827
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.7", type: AuthFormComponent, isStandalone: false, selector: "hc-auth-card", inputs: { local: { classPropertyName: "local", publicName: "local", isSignal: true, isRequired: false, transformFunction: null }, google: { classPropertyName: "google", publicName: "google", isSignal: true, isRequired: false, transformFunction: null }, facebook: { classPropertyName: "facebook", publicName: "facebook", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onError: "onError", onSignIn: "onSignIn", onSignUp: "onSignUp" }, ngImport: i0, template: "<!--<div class=\"auth-container d-flex justify-content-center align-items-center\">-->\r\n<!-- -->\r\n<!--</div>-->\r\n<hc-card>\r\n <!-- @if (isLoading()) {-->\r\n <!-- <div class=\"loading-overlay w-100 h-100\"></div>-->\r\n <!-- }-->\r\n <form\r\n class=\"d-inline-flex flex-column align-items-center w-100\"\r\n [formGroup]=\"authForm\"\r\n (ngSubmit)=\"handleSubmit($event)\"\r\n >\r\n @if (isSignUp()) {\r\n <div class=\"form-group w-100\">\r\n <label for=\"firstName\" class=\"form-label\">First Name</label>\r\n <input\r\n id=\"firstName\"\r\n type=\"text\"\r\n class=\"form-control w-100\"\r\n formControlName=\"firstName\"\r\n placeholder=\"Enter your first name\"\r\n />\r\n </div>\r\n\r\n <div class=\"form-group w-100\">\r\n <label for=\"lastName\" class=\"form-label\">Last Name</label>\r\n <input\r\n id=\"lastName\"\r\n type=\"text\"\r\n class=\"form-control w-100\"\r\n formControlName=\"lastName\"\r\n placeholder=\"Enter your last name\"\r\n />\r\n </div>\r\n }\r\n\r\n <div class=\"form-group w-100\">\r\n <label for=\"authField\" class=\"form-label\">{{ authFieldLabel() }}</label>\r\n <input\r\n id=\"authField\"\r\n type=\"text\"\r\n class=\"form-control w-100\"\r\n formControlName=\"authFieldValue\"\r\n [placeholder]=\"'Enter your ' + authFieldLabel().toLowerCase()\"\r\n />\r\n </div>\r\n\r\n <div class=\"form-group w-100\">\r\n <label for=\"password\" class=\"form-label\">Password</label>\r\n <input\r\n id=\"password\"\r\n type=\"password\"\r\n class=\"form-control w-100\"\r\n formControlName=\"password\"\r\n placeholder=\"Enter your password\"\r\n />\r\n </div>\r\n\r\n <button type=\"submit\" class=\"btn btn-primary mb-3 w-100\" [disabled]=\"isLoading()\">\r\n {{ isSignUp() ? (isLoading() ? \"Signing Up...\" : \"Sign Up\") : isLoading() ? \"Signing In...\" : \"Sign In\" }}\r\n </button>\r\n\r\n <button type=\"button\" class=\"btn btn-link p-0 mb-3\" (click)=\"isSignUp.set(!isSignUp())\">\r\n {{ isSignUp() ? \"Already have an account? Sign In\" : \"Don't have an account? Sign Up\" }}\r\n </button>\r\n\r\n @if (!isSignUp()) {\r\n @if (local() && (google() || facebook())) {\r\n <hc-separator label=\"OR\"></hc-separator>\r\n }\r\n\r\n @if (google()) {\r\n <button type=\"button\" class=\"btn google-btn btn-light mb-3 w-100\" (click)=\"handleGoogleSignIn()\">\r\n <div class=\"icon\"></div>\r\n Sign in with Google\r\n </button>\r\n }\r\n\r\n @if (facebook()) {\r\n <button type=\"button\" class=\"btn facebook-btn btn-light mb-3 w-100\">\r\n <div class=\"icon\"></div>\r\n Sign in with Facebook\r\n </button>\r\n }\r\n\r\n @if (isError()) {\r\n <div class=\"error-message w-100\">\r\n {{ error()?.error?.message || \"Something went wrong!\" }}\r\n </div>\r\n }\r\n }\r\n </form>\r\n</hc-card>\r\n", styles: [".auth-form{background:#fffffff2;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);border-radius:16px;margin:8px;padding:2rem;position:relative;z-index:1}.loading-overlay{background:#ffffffe6;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);border-radius:16px;display:flex;align-items:center;justify-content:center;font-weight:600;color:var(--secondary-color);font-size:1.1rem;position:absolute;top:0;left:0}.loading-overlay:after{content:\"\";width:50px;height:50px;border:5px solid var(--secondary-color);border-top:5px solid transparent;border-radius:50%;animation:spin 1s linear infinite;margin-left:10px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.form-group{position:relative;margin-bottom:1.5rem}.form-label{font-weight:600;color:var(--text-color);margin-bottom:.5rem;font-size:.9rem;text-transform:uppercase;letter-spacing:.5px}.form-control{border:2px solid var(--border-color);border-radius:12px;padding:.75rem 1rem;font-size:1rem;transition:all .3s ease;background:#fffc;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.form-control:focus{border-color:var(--secondary-color);box-shadow:0 0 0 3px #667eea1a;background:#fffffff2}.form-control::placeholder{color:var(--text-light)}.btn{border-radius:12px;padding:.75rem 1.5rem;font-weight:600;font-size:1rem;transition:all .3s ease;border:none;position:relative;overflow:hidden}.btn:before{content:\"\";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.2),transparent);transition:left .5s}.btn:hover:before{left:100%}.btn-primary{background:var(--secondary-gradient);color:#fff;box-shadow:var(--shadow-md)}.btn-primary:hover{box-shadow:var(--shadow-lg)}.btn-link{color:var(--text-light);text-decoration:none;font-size:.9rem;transition:all .3s ease}.btn-link:hover{color:var(--secondary-color);text-decoration:underline}.google-btn,.facebook-btn{display:flex;align-items:center;justify-content:center;gap:12px;color:var(--text-color);background:#ffffffe6;border:2px solid var(--border-color);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);font-weight:600;transition:all .3s ease}.google-btn:hover,.facebook-btn:hover{background:#fff;border-color:var(--secondary-color);box-shadow:var(--shadow-md)}.google-btn .icon,.facebook-btn .icon{height:24px;width:24px;background-repeat:no-repeat;background-size:contain;background-position:center}.google-btn .icon{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMTcuNiA5LjJsLS4xLTEuOEg5djMuNGg0LjhDMTMuNiAxMiAxMyAxMyAxMiAxMy42djIuMmgzYTguOCA4LjggMCAwIDAgMi42LTYuNnoiIGZpbGw9IiM0Mjg1RjQiIGZpbGwtcnVsZT0ibm9uemVybyIvPjxwYXRoIGQ9Ik05IDE4YzIuNCAwIDQuNS0uOCA2LTIuMmwtMy0yLjJhNS40IDUuNCAwIDAgMS04LTIuOUgxVjEzYTkgOSAwIDAgMCA4IDV6IiBmaWxsPSIjMzRBODUzIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNNCAxMC43YTUuNCA1LjQgMCAwIDEgMC0zLjRWNUgxYTkgOSAwIDAgMCAwIDhsMy0yLjN6IiBmaWxsPSIjRkJCQzA1IiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNOSAzLjZjMS4zIDAgMi41LjQgMy40IDEuM0wxNSAyLjNBOSA5IDAgMCAwIDEgNWwzIDIuNGE1LjQgNS40IDAgMCAxIDUtMy43eiIgZmlsbD0iI0VBNDMzNSIgZmlsbC1ydWxlPSJub256ZXJvIi8+PHBhdGggZD0iTTAgMGgxOHYxOEgweiIvPjwvZz48L3N2Zz4=)}.facebook-btn .icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAmJJREFUaEPtmj9oFEEUxr9v7nIpzkIRLQQVFLUSBBs7IbfGwkrhtNHbvYiiRTqbNKIRbGwD/iFkd88uISmsEtwErRQsLEVUiGIbsDAK8W6e3IESkuzt3eVucwMz7b5hvt/73rwZluHwtcr+Wrb6BMJhgnkYMASyKlotDGZqt1goT81R1EUDdG+SqDXnWPD8n6ZkfiNB3Qk6XiAmZv+fZguw0+5ZBzp3QITAOw1EiupDrYYVZvFHaZ0DkNfCHCn7ILwAwolbZ0ccEMgCtRqLKu77pAQ45eAOBI/6CID3oqA0DrCl7tdfAIKJKPRGk7K+/nsfAci3Pav5YzMzl9fMBCBHI9+daEd8PbZvHKhmswdfTV79biSACD4tht7xOPHF4nTux65fD7RIEcIDJAZbBU2ljYrm0mLFLcSJKpSD+xTcbVX0+rhUADT19JI/ciUWwAveEDjTtwAAn0eBW4oT6LjBFxBHzAUohctQctgCdJKB1uYklJB1oLU0JkYJMZ7ReLExUFFW5oPycuwm9vyTSli/Rm8aNWKSwKmUbqNyPQrKU4mkbQQ4nv8V4CEjAU7ffDqwey33m2DGSIAhNziqiM/NDOvySdzdEiqMhA61vDQWwPH8GwCfGQtwzg0eCjGWGoCGvFbkxy0WfBv5nh8npCFUYe8WPfQslJxIDSB+IXsSx+amy10otls3v07bu1AbR3tnoXYP2D3QWeX8n2VLyJaQLaGm/4XsQbbNAkmebrtQfBdK56lBbxxoPDUYKoWzSsml5DLYTkRvAADMsv7cpkp5TKXP9+7RR3cBGpkH5weob/8FwaStQs990hUAAAAASUVORK5CYII=)}.separator{position:relative;width:calc(100% - 40px);height:1px;background:linear-gradient(90deg,transparent,var(--border-color),transparent);margin:2rem auto}.separator:after{content:\"OR\";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#ffffffe6;padding:0 1rem;font-size:.8rem;font-weight:600;color:var(--text-light);letter-spacing:1px}.error-message{color:var(--danger-color);margin-top:1rem;font-weight:600;text-align:center}@keyframes slideIn{0%{opacity:0}to{opacity:1}}@media (max-width: 480px){.auth-container{margin:1rem;border-radius:16px}.auth-form{padding:1.5rem;margin:4px}.btn{padding:.875rem 1.25rem;font-size:.95rem}}\n"], dependencies: [{ kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.HcCardComponent, selector: "hc-card" }, { kind: "component", type: i3.HcSeparatorComponent, selector: "hc-separator", inputs: ["label"] }] });
|
|
828
|
+
}
|
|
829
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AuthFormComponent, decorators: [{
|
|
830
|
+
type: Component,
|
|
831
|
+
args: [{ selector: "hc-auth-card", standalone: false, template: "<!--<div class=\"auth-container d-flex justify-content-center align-items-center\">-->\r\n<!-- -->\r\n<!--</div>-->\r\n<hc-card>\r\n <!-- @if (isLoading()) {-->\r\n <!-- <div class=\"loading-overlay w-100 h-100\"></div>-->\r\n <!-- }-->\r\n <form\r\n class=\"d-inline-flex flex-column align-items-center w-100\"\r\n [formGroup]=\"authForm\"\r\n (ngSubmit)=\"handleSubmit($event)\"\r\n >\r\n @if (isSignUp()) {\r\n <div class=\"form-group w-100\">\r\n <label for=\"firstName\" class=\"form-label\">First Name</label>\r\n <input\r\n id=\"firstName\"\r\n type=\"text\"\r\n class=\"form-control w-100\"\r\n formControlName=\"firstName\"\r\n placeholder=\"Enter your first name\"\r\n />\r\n </div>\r\n\r\n <div class=\"form-group w-100\">\r\n <label for=\"lastName\" class=\"form-label\">Last Name</label>\r\n <input\r\n id=\"lastName\"\r\n type=\"text\"\r\n class=\"form-control w-100\"\r\n formControlName=\"lastName\"\r\n placeholder=\"Enter your last name\"\r\n />\r\n </div>\r\n }\r\n\r\n <div class=\"form-group w-100\">\r\n <label for=\"authField\" class=\"form-label\">{{ authFieldLabel() }}</label>\r\n <input\r\n id=\"authField\"\r\n type=\"text\"\r\n class=\"form-control w-100\"\r\n formControlName=\"authFieldValue\"\r\n [placeholder]=\"'Enter your ' + authFieldLabel().toLowerCase()\"\r\n />\r\n </div>\r\n\r\n <div class=\"form-group w-100\">\r\n <label for=\"password\" class=\"form-label\">Password</label>\r\n <input\r\n id=\"password\"\r\n type=\"password\"\r\n class=\"form-control w-100\"\r\n formControlName=\"password\"\r\n placeholder=\"Enter your password\"\r\n />\r\n </div>\r\n\r\n <button type=\"submit\" class=\"btn btn-primary mb-3 w-100\" [disabled]=\"isLoading()\">\r\n {{ isSignUp() ? (isLoading() ? \"Signing Up...\" : \"Sign Up\") : isLoading() ? \"Signing In...\" : \"Sign In\" }}\r\n </button>\r\n\r\n <button type=\"button\" class=\"btn btn-link p-0 mb-3\" (click)=\"isSignUp.set(!isSignUp())\">\r\n {{ isSignUp() ? \"Already have an account? Sign In\" : \"Don't have an account? Sign Up\" }}\r\n </button>\r\n\r\n @if (!isSignUp()) {\r\n @if (local() && (google() || facebook())) {\r\n <hc-separator label=\"OR\"></hc-separator>\r\n }\r\n\r\n @if (google()) {\r\n <button type=\"button\" class=\"btn google-btn btn-light mb-3 w-100\" (click)=\"handleGoogleSignIn()\">\r\n <div class=\"icon\"></div>\r\n Sign in with Google\r\n </button>\r\n }\r\n\r\n @if (facebook()) {\r\n <button type=\"button\" class=\"btn facebook-btn btn-light mb-3 w-100\">\r\n <div class=\"icon\"></div>\r\n Sign in with Facebook\r\n </button>\r\n }\r\n\r\n @if (isError()) {\r\n <div class=\"error-message w-100\">\r\n {{ error()?.error?.message || \"Something went wrong!\" }}\r\n </div>\r\n }\r\n }\r\n </form>\r\n</hc-card>\r\n", styles: [".auth-form{background:#fffffff2;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);border-radius:16px;margin:8px;padding:2rem;position:relative;z-index:1}.loading-overlay{background:#ffffffe6;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);border-radius:16px;display:flex;align-items:center;justify-content:center;font-weight:600;color:var(--secondary-color);font-size:1.1rem;position:absolute;top:0;left:0}.loading-overlay:after{content:\"\";width:50px;height:50px;border:5px solid var(--secondary-color);border-top:5px solid transparent;border-radius:50%;animation:spin 1s linear infinite;margin-left:10px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.form-group{position:relative;margin-bottom:1.5rem}.form-label{font-weight:600;color:var(--text-color);margin-bottom:.5rem;font-size:.9rem;text-transform:uppercase;letter-spacing:.5px}.form-control{border:2px solid var(--border-color);border-radius:12px;padding:.75rem 1rem;font-size:1rem;transition:all .3s ease;background:#fffc;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.form-control:focus{border-color:var(--secondary-color);box-shadow:0 0 0 3px #667eea1a;background:#fffffff2}.form-control::placeholder{color:var(--text-light)}.btn{border-radius:12px;padding:.75rem 1.5rem;font-weight:600;font-size:1rem;transition:all .3s ease;border:none;position:relative;overflow:hidden}.btn:before{content:\"\";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.2),transparent);transition:left .5s}.btn:hover:before{left:100%}.btn-primary{background:var(--secondary-gradient);color:#fff;box-shadow:var(--shadow-md)}.btn-primary:hover{box-shadow:var(--shadow-lg)}.btn-link{color:var(--text-light);text-decoration:none;font-size:.9rem;transition:all .3s ease}.btn-link:hover{color:var(--secondary-color);text-decoration:underline}.google-btn,.facebook-btn{display:flex;align-items:center;justify-content:center;gap:12px;color:var(--text-color);background:#ffffffe6;border:2px solid var(--border-color);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);font-weight:600;transition:all .3s ease}.google-btn:hover,.facebook-btn:hover{background:#fff;border-color:var(--secondary-color);box-shadow:var(--shadow-md)}.google-btn .icon,.facebook-btn .icon{height:24px;width:24px;background-repeat:no-repeat;background-size:contain;background-position:center}.google-btn .icon{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMTcuNiA5LjJsLS4xLTEuOEg5djMuNGg0LjhDMTMuNiAxMiAxMyAxMyAxMiAxMy42djIuMmgzYTguOCA4LjggMCAwIDAgMi42LTYuNnoiIGZpbGw9IiM0Mjg1RjQiIGZpbGwtcnVsZT0ibm9uemVybyIvPjxwYXRoIGQ9Ik05IDE4YzIuNCAwIDQuNS0uOCA2LTIuMmwtMy0yLjJhNS40IDUuNCAwIDAgMS04LTIuOUgxVjEzYTkgOSAwIDAgMCA4IDV6IiBmaWxsPSIjMzRBODUzIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNNCAxMC43YTUuNCA1LjQgMCAwIDEgMC0zLjRWNUgxYTkgOSAwIDAgMCAwIDhsMy0yLjN6IiBmaWxsPSIjRkJCQzA1IiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNOSAzLjZjMS4zIDAgMi41LjQgMy40IDEuM0wxNSAyLjNBOSA5IDAgMCAwIDEgNWwzIDIuNGE1LjQgNS40IDAgMCAxIDUtMy43eiIgZmlsbD0iI0VBNDMzNSIgZmlsbC1ydWxlPSJub256ZXJvIi8+PHBhdGggZD0iTTAgMGgxOHYxOEgweiIvPjwvZz48L3N2Zz4=)}.facebook-btn .icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAmJJREFUaEPtmj9oFEEUxr9v7nIpzkIRLQQVFLUSBBs7IbfGwkrhtNHbvYiiRTqbNKIRbGwD/iFkd88uISmsEtwErRQsLEVUiGIbsDAK8W6e3IESkuzt3eVucwMz7b5hvt/73rwZluHwtcr+Wrb6BMJhgnkYMASyKlotDGZqt1goT81R1EUDdG+SqDXnWPD8n6ZkfiNB3Qk6XiAmZv+fZguw0+5ZBzp3QITAOw1EiupDrYYVZvFHaZ0DkNfCHCn7ILwAwolbZ0ccEMgCtRqLKu77pAQ45eAOBI/6CID3oqA0DrCl7tdfAIKJKPRGk7K+/nsfAci3Pav5YzMzl9fMBCBHI9+daEd8PbZvHKhmswdfTV79biSACD4tht7xOPHF4nTux65fD7RIEcIDJAZbBU2ljYrm0mLFLcSJKpSD+xTcbVX0+rhUADT19JI/ciUWwAveEDjTtwAAn0eBW4oT6LjBFxBHzAUohctQctgCdJKB1uYklJB1oLU0JkYJMZ7ReLExUFFW5oPycuwm9vyTSli/Rm8aNWKSwKmUbqNyPQrKU4mkbQQ4nv8V4CEjAU7ffDqwey33m2DGSIAhNziqiM/NDOvySdzdEiqMhA61vDQWwPH8GwCfGQtwzg0eCjGWGoCGvFbkxy0WfBv5nh8npCFUYe8WPfQslJxIDSB+IXsSx+amy10otls3v07bu1AbR3tnoXYP2D3QWeX8n2VLyJaQLaGm/4XsQbbNAkmebrtQfBdK56lBbxxoPDUYKoWzSsml5DLYTkRvAADMsv7cpkp5TKXP9+7RR3cBGpkH5weob/8FwaStQs990hUAAAAASUVORK5CYII=)}.separator{position:relative;width:calc(100% - 40px);height:1px;background:linear-gradient(90deg,transparent,var(--border-color),transparent);margin:2rem auto}.separator:after{content:\"OR\";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#ffffffe6;padding:0 1rem;font-size:.8rem;font-weight:600;color:var(--text-light);letter-spacing:1px}.error-message{color:var(--danger-color);margin-top:1rem;font-weight:600;text-align:center}@keyframes slideIn{0%{opacity:0}to{opacity:1}}@media (max-width: 480px){.auth-container{margin:1rem;border-radius:16px}.auth-form{padding:1.5rem;margin:4px}.btn{padding:.875rem 1.25rem;font-size:.95rem}}\n"] }]
|
|
832
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
833
|
+
type: Inject,
|
|
834
|
+
args: [AUTH_CONFIG]
|
|
835
|
+
}] }, { type: i1$1.FormBuilder }, { type: AuthService }] });
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Angular structural directive for permission-based conditional rendering
|
|
839
|
+
*
|
|
840
|
+
* This directive conditionally displays or hides DOM elements based on the current user's permissions.
|
|
841
|
+
* It integrates with the authentication state to check if the authenticated user has the required
|
|
842
|
+
* permission to view the content. The directive uses Angular's structural directive pattern and
|
|
843
|
+
* automatically updates when the user's authentication state or permissions change.
|
|
844
|
+
*
|
|
845
|
+
* The directive works by checking the user's role permissions against the required permission string.
|
|
846
|
+
* If the user has the required permission, the template content is rendered; otherwise, it's removed
|
|
847
|
+
* from the DOM.
|
|
848
|
+
*
|
|
849
|
+
* @example
|
|
850
|
+
* ```html
|
|
851
|
+
* <!-- Basic usage - show content only if user has 'users.read' permission -->
|
|
852
|
+
* <div *hcPermission="'users.read'">
|
|
853
|
+
* <p>This content is only visible to users with read permission</p>
|
|
854
|
+
* </div>
|
|
855
|
+
* ```
|
|
856
|
+
*
|
|
857
|
+
* @example
|
|
858
|
+
* ```html
|
|
859
|
+
* <!-- Using with component properties -->
|
|
860
|
+
* <button *hcPermission="'users.delete'" (click)="deleteUser()">
|
|
861
|
+
* Delete User
|
|
862
|
+
* </button>
|
|
863
|
+
* ```
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* ```html
|
|
867
|
+
* <!-- Using with multiple permissions (user needs at least one) -->
|
|
868
|
+
* <div *hcPermission="['users.read', 'users.write']">
|
|
869
|
+
* <p>This content is visible to users with either read OR write permission</p>
|
|
870
|
+
* </div>
|
|
871
|
+
* ```
|
|
872
|
+
*
|
|
873
|
+
* @example
|
|
874
|
+
* ```html
|
|
875
|
+
* <!-- Using with dynamic permissions -->
|
|
876
|
+
* <ng-container *hcPermission="requiredPermission">
|
|
877
|
+
* <app-admin-panel></app-admin-panel>
|
|
878
|
+
* </ng-container>
|
|
879
|
+
* ```
|
|
880
|
+
*
|
|
881
|
+
* @example
|
|
882
|
+
* ```typescript
|
|
883
|
+
* // Component usage with dynamic permission
|
|
884
|
+
* export class UserListComponent {
|
|
885
|
+
* requiredPermission = 'users.manage';
|
|
886
|
+
* // Or with multiple permissions
|
|
887
|
+
* requiredPermissions = ['users.read', 'users.write'];
|
|
888
|
+
* }
|
|
889
|
+
* ```
|
|
890
|
+
*
|
|
891
|
+
* @see {@link AuthState} Authentication state service that provides user information
|
|
892
|
+
* @see {@link User} User interface that contains role and permission information
|
|
893
|
+
* @see {@link isRoleObject} Utility function to check if role is an object with permissions
|
|
894
|
+
* @see {@link NgxHichchiAuthModule} Module that provides this directive
|
|
895
|
+
*/
|
|
896
|
+
class PermissionDirective {
|
|
897
|
+
/**
|
|
898
|
+
* Template reference for the content to be conditionally rendered
|
|
899
|
+
* @private
|
|
900
|
+
*/
|
|
901
|
+
templateRef = inject(TemplateRef);
|
|
902
|
+
/**
|
|
903
|
+
* View container reference for managing the template rendering
|
|
904
|
+
* @private
|
|
905
|
+
*/
|
|
906
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
907
|
+
/**
|
|
908
|
+
* Authentication state service for accessing current user information
|
|
909
|
+
* @private
|
|
910
|
+
*/
|
|
911
|
+
authState = inject(AuthState);
|
|
912
|
+
/**
|
|
913
|
+
* Required permission string or array of strings input signal
|
|
914
|
+
*
|
|
915
|
+
* This input defines the permission(s) that the current user must have
|
|
916
|
+
* for the template content to be displayed. The permission string(s)
|
|
917
|
+
* should match the permissions defined in the user's role.
|
|
918
|
+
*
|
|
919
|
+
* When an array is provided, the user needs to have at least one of
|
|
920
|
+
* the specified permissions (OR logic).
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* ```html
|
|
924
|
+
* <!-- Single permission -->
|
|
925
|
+
* <div *hcPermission="'users.read'">Content</div>
|
|
926
|
+
*
|
|
927
|
+
* <!-- Multiple permissions (user needs at least one) -->
|
|
928
|
+
* <div *hcPermission="['users.read', 'users.write']">Content</div>
|
|
929
|
+
* ```
|
|
930
|
+
*/
|
|
931
|
+
hcPermission = input.required();
|
|
932
|
+
/**
|
|
933
|
+
* Constructor that sets up the permission checking effect
|
|
934
|
+
*
|
|
935
|
+
* Initializes an Angular effect that automatically re-evaluates permission
|
|
936
|
+
* whenever the authentication state or required permission changes. This
|
|
937
|
+
* ensures the UI stays in sync with the user's current permissions.
|
|
938
|
+
*/
|
|
939
|
+
constructor() {
|
|
940
|
+
effect(() => {
|
|
941
|
+
if (this.hasPermission(this.authState.user(), this.hcPermission())) {
|
|
942
|
+
if (this.viewContainerRef.length === 0) {
|
|
943
|
+
this.viewContainerRef.createEmbeddedView(this.templateRef);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
this.viewContainerRef.clear();
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Checks if the user has the required permission(s)
|
|
953
|
+
*
|
|
954
|
+
* This method evaluates whether the provided user has the specified permission(s)
|
|
955
|
+
* by checking their role and associated permissions. It handles cases where
|
|
956
|
+
* the user is null, has no role, or the role doesn't contain permissions.
|
|
957
|
+
*
|
|
958
|
+
* When an array of permissions is provided, the method returns true if the user
|
|
959
|
+
* has at least one of the specified permissions (OR logic).
|
|
960
|
+
*
|
|
961
|
+
* @param user - The user object to check permissions for, can be null
|
|
962
|
+
* @param requiredPermission - The permission string or array of strings that must be present in the user's role
|
|
963
|
+
* @returns True if the user has the required permission(s), false otherwise
|
|
964
|
+
*
|
|
965
|
+
* @example
|
|
966
|
+
* ```typescript
|
|
967
|
+
* // Single permission
|
|
968
|
+
* const hasPermission = this.hasPermission(currentUser, 'users.delete');
|
|
969
|
+
*
|
|
970
|
+
* // Multiple permissions (user needs at least one)
|
|
971
|
+
* const hasAnyPermission = this.hasPermission(currentUser, ['users.read', 'users.write']);
|
|
972
|
+
* ```
|
|
973
|
+
*
|
|
974
|
+
* @private
|
|
975
|
+
*/
|
|
976
|
+
hasPermission(user, requiredPermission) {
|
|
977
|
+
if (!user || !user.role) {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
return isRoleObject(user.role) && user.role.permissions?.length
|
|
981
|
+
? Array.isArray(requiredPermission)
|
|
982
|
+
? requiredPermission.some(permission => user.role.permissions?.includes(permission))
|
|
983
|
+
: user.role.permissions.includes(requiredPermission)
|
|
984
|
+
: false;
|
|
985
|
+
}
|
|
986
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: PermissionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
987
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.0.7", type: PermissionDirective, isStandalone: true, selector: "[hcPermission]", inputs: { hcPermission: { classPropertyName: "hcPermission", publicName: "hcPermission", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
988
|
+
}
|
|
989
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: PermissionDirective, decorators: [{
|
|
990
|
+
type: Directive,
|
|
991
|
+
args: [{
|
|
992
|
+
selector: "[hcPermission]",
|
|
993
|
+
}]
|
|
994
|
+
}], ctorParameters: () => [] });
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Enumeration of authentication guard conditions
|
|
998
|
+
*
|
|
999
|
+
* This enum defines the different conditions that authentication guards can check
|
|
1000
|
+
* to determine whether a user should be allowed to access a route. Each condition
|
|
1001
|
+
* represents a different aspect of the user's authentication state.
|
|
1002
|
+
*
|
|
1003
|
+
* These conditions are used in conjunction with AuthGuardOption to create
|
|
1004
|
+
* flexible route protection rules that can handle various authentication scenarios.
|
|
1005
|
+
*
|
|
1006
|
+
* @example
|
|
1007
|
+
* ```typescript
|
|
1008
|
+
* // Check if user is signed in
|
|
1009
|
+
* const guardOption: AuthGuardOption = {
|
|
1010
|
+
* condition: AuthGuardCondition.SIGNED_IN,
|
|
1011
|
+
* state: true,
|
|
1012
|
+
* redirect: '/login'
|
|
1013
|
+
* };
|
|
1014
|
+
* ```
|
|
1015
|
+
*
|
|
1016
|
+
* @example
|
|
1017
|
+
* ```typescript
|
|
1018
|
+
* // Check if user has a valid token
|
|
1019
|
+
* const tokenGuardOption: AuthGuardOption = {
|
|
1020
|
+
* condition: AuthGuardCondition.HAS_TOKEN,
|
|
1021
|
+
* state: true,
|
|
1022
|
+
* redirect: '/unauthorized'
|
|
1023
|
+
* };
|
|
1024
|
+
* ```
|
|
1025
|
+
*
|
|
1026
|
+
* @see {@link AuthGuardOption} Interface that uses these conditions
|
|
1027
|
+
*/
|
|
1028
|
+
var AuthGuardCondition;
|
|
1029
|
+
(function (AuthGuardCondition) {
|
|
1030
|
+
/** Check if the user is signed in to the application */
|
|
1031
|
+
AuthGuardCondition["SIGNED_IN"] = "signed-in";
|
|
1032
|
+
/** Check if the user has a valid access token */
|
|
1033
|
+
AuthGuardCondition["HAS_TOKEN"] = "has-token";
|
|
1034
|
+
})(AuthGuardCondition || (AuthGuardCondition = {}));
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Retrieves all authentication guard options from a route and its parent routes
|
|
1038
|
+
*
|
|
1039
|
+
* This utility function extracts authentication guard options from the current route
|
|
1040
|
+
* and recursively collects options from parent routes in the route hierarchy. It handles
|
|
1041
|
+
* the inheritance and merging of guard options, ensuring that parent route guards are
|
|
1042
|
+
* applied alongside child route guards.
|
|
1043
|
+
*
|
|
1044
|
+
* The function implements a hierarchical guard system where:
|
|
1045
|
+
* - Child route options take precedence over parent options for the same condition
|
|
1046
|
+
* - Parent options are inherited when no conflicting child option exists
|
|
1047
|
+
* - Options are collected recursively up the route tree
|
|
1048
|
+
* - The final array contains all applicable guard options for the route
|
|
1049
|
+
*
|
|
1050
|
+
* This enables complex authentication scenarios where different route levels can
|
|
1051
|
+
* specify different authentication requirements, with child routes able to override
|
|
1052
|
+
* or supplement parent route authentication rules.
|
|
1053
|
+
*
|
|
1054
|
+
* @param currentRoute - The activated route snapshot to extract guard options from
|
|
1055
|
+
* @returns Array of authentication guard options applicable to the route
|
|
1056
|
+
*
|
|
1057
|
+
* @example
|
|
1058
|
+
* ```typescript
|
|
1059
|
+
* // Route configuration with nested guards
|
|
1060
|
+
* const routes: Routes = [
|
|
1061
|
+
* {
|
|
1062
|
+
* path: 'admin',
|
|
1063
|
+
* component: AdminLayoutComponent,
|
|
1064
|
+
* canActivate: [authGuard(AuthGuardCondition.SIGNED_IN, true, '/login')],
|
|
1065
|
+
* children: [
|
|
1066
|
+
* {
|
|
1067
|
+
* path: 'users',
|
|
1068
|
+
* component: UsersComponent,
|
|
1069
|
+
* canActivate: [authGuard(AuthGuardCondition.HAS_TOKEN, true, '/unauthorized')]
|
|
1070
|
+
* }
|
|
1071
|
+
* ]
|
|
1072
|
+
* }
|
|
1073
|
+
* ];
|
|
1074
|
+
*
|
|
1075
|
+
* // In the guard, get all applicable options
|
|
1076
|
+
* const allOptions = getAllAuthGuardOptions(route);
|
|
1077
|
+
* // Result: [
|
|
1078
|
+
* // { condition: 'signed-in', state: true, redirect: '/login' },
|
|
1079
|
+
* // { condition: 'has-token', state: true, redirect: '/unauthorized' }
|
|
1080
|
+
* // ]
|
|
1081
|
+
* ```
|
|
1082
|
+
*
|
|
1083
|
+
* @example
|
|
1084
|
+
* ```typescript
|
|
1085
|
+
* // Using in a custom guard implementation
|
|
1086
|
+
* export const customAuthGuard: CanActivateFn = (route, state) => {
|
|
1087
|
+
* const authState = inject(AuthState);
|
|
1088
|
+
* const router = inject(Router);
|
|
1089
|
+
*
|
|
1090
|
+
* const guardOptions = getAllAuthGuardOptions(route);
|
|
1091
|
+
*
|
|
1092
|
+
* for (const option of guardOptions) {
|
|
1093
|
+
* const conditionMet = checkAuthCondition(option.condition, authState);
|
|
1094
|
+
* if (option.state !== conditionMet) {
|
|
1095
|
+
* router.navigateByUrl(option.redirect);
|
|
1096
|
+
* return false;
|
|
1097
|
+
* }
|
|
1098
|
+
* }
|
|
1099
|
+
*
|
|
1100
|
+
* return true;
|
|
1101
|
+
* };
|
|
1102
|
+
* ```
|
|
1103
|
+
*
|
|
1104
|
+
* @example
|
|
1105
|
+
* ```typescript
|
|
1106
|
+
* // Route hierarchy with inherited guards
|
|
1107
|
+
* const routes: Routes = [
|
|
1108
|
+
* {
|
|
1109
|
+
* path: 'app',
|
|
1110
|
+
* data: { [AUTH_GUARD_OPTIONS_KEY]: [
|
|
1111
|
+
* { condition: AuthGuardCondition.SIGNED_IN, state: true, redirect: '/login' }
|
|
1112
|
+
* ]},
|
|
1113
|
+
* children: [
|
|
1114
|
+
* {
|
|
1115
|
+
* path: 'profile',
|
|
1116
|
+
* component: ProfileComponent,
|
|
1117
|
+
* data: { [AUTH_GUARD_OPTIONS_KEY]: [
|
|
1118
|
+
* { condition: AuthGuardCondition.HAS_TOKEN, state: true, redirect: '/expired' }
|
|
1119
|
+
* ]}
|
|
1120
|
+
* }
|
|
1121
|
+
* ]
|
|
1122
|
+
* }
|
|
1123
|
+
* ];
|
|
1124
|
+
*
|
|
1125
|
+
* // When accessing /app/profile, both parent and child guards apply
|
|
1126
|
+
* ```
|
|
1127
|
+
*
|
|
1128
|
+
* @see {@link AuthGuardOption} Interface defining the structure of guard options
|
|
1129
|
+
* @see {@link AUTH_GUARD_OPTIONS_KEY} Constant for the route data key
|
|
1130
|
+
* @see {@link authGuard} Function that uses this utility to process guard options
|
|
1131
|
+
* @see {@link ActivatedRouteSnapshot} Angular router interface for route snapshots
|
|
1132
|
+
*/
|
|
1133
|
+
const getAllAuthGuardOptions = (currentRoute) => {
|
|
1134
|
+
const options = currentRoute.data?.[AUTH_GUARD_OPTIONS_KEY] || [];
|
|
1135
|
+
if (!currentRoute.parent?.data?.[AUTH_GUARD_OPTIONS_KEY]) {
|
|
1136
|
+
return options;
|
|
1137
|
+
}
|
|
1138
|
+
const parentOptions = getAllAuthGuardOptions(currentRoute.parent);
|
|
1139
|
+
for (const parentOption of parentOptions) {
|
|
1140
|
+
const currentConditionIndex = options.findIndex(option => option.condition === parentOption.condition);
|
|
1141
|
+
if (currentConditionIndex !== -1) {
|
|
1142
|
+
// options[currentConditionIndex] = parentOption;
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
options.unshift(parentOption);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return options;
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Authentication guard factory function for Angular route protection
|
|
1153
|
+
*
|
|
1154
|
+
* This function creates a route guard that protects routes based on authentication state.
|
|
1155
|
+
* It supports both simple single-condition guards and complex multi-condition guards.
|
|
1156
|
+
* The guard evaluates authentication conditions and redirects users when conditions are not met.
|
|
1157
|
+
*
|
|
1158
|
+
* The guard integrates with the AuthState service to check the current authentication status
|
|
1159
|
+
* and uses the Angular Router for navigation when redirects are needed. It supports checking
|
|
1160
|
+
* whether users are signed in, have valid tokens, and other authentication-related conditions.
|
|
1161
|
+
*
|
|
1162
|
+
* Key features:
|
|
1163
|
+
* - Multiple authentication condition support
|
|
1164
|
+
* - Automatic redirection on failed conditions
|
|
1165
|
+
* - Integration with AuthState for reactive authentication checks
|
|
1166
|
+
* - Support for both simple and complex guard configurations
|
|
1167
|
+
* - Type-safe condition checking
|
|
1168
|
+
*
|
|
1169
|
+
* @param param - Either a single AuthGuardCondition or an array of AuthGuardOption objects
|
|
1170
|
+
* @param state - Required state for single condition (ignored when using options array)
|
|
1171
|
+
* @param redirect - Redirect path for single condition (ignored when using options array)
|
|
1172
|
+
* @returns A CanActivateFn that evaluates authentication conditions and handles navigation
|
|
1173
|
+
*
|
|
1174
|
+
* @example
|
|
1175
|
+
* ```typescript
|
|
1176
|
+
* // Protecting a route that requires authentication
|
|
1177
|
+
* const routes: Routes = [
|
|
1178
|
+
* {
|
|
1179
|
+
* path: 'dashboard',
|
|
1180
|
+
* component: DashboardComponent,
|
|
1181
|
+
* canActivate: [authGuard(AuthGuardCondition.SIGNED_IN, true, '/login')]
|
|
1182
|
+
* }
|
|
1183
|
+
* ];
|
|
1184
|
+
* ```
|
|
1185
|
+
*
|
|
1186
|
+
* @example
|
|
1187
|
+
* ```typescript
|
|
1188
|
+
* // Complex guard with multiple conditions
|
|
1189
|
+
* const routes: Routes = [
|
|
1190
|
+
* {
|
|
1191
|
+
* path: 'admin',
|
|
1192
|
+
* component: AdminComponent,
|
|
1193
|
+
* canActivate: [authGuard([
|
|
1194
|
+
* { condition: AuthGuardCondition.SIGNED_IN, state: true, redirect: '/login' },
|
|
1195
|
+
* { condition: AuthGuardCondition.HAS_TOKEN, state: true, redirect: '/unauthorized' }
|
|
1196
|
+
* ])]
|
|
1197
|
+
* }
|
|
1198
|
+
* ];
|
|
1199
|
+
* ```
|
|
1200
|
+
*
|
|
1201
|
+
* @example
|
|
1202
|
+
* ```typescript
|
|
1203
|
+
* // Preventing authenticated users from accessing login page
|
|
1204
|
+
* const routes: Routes = [
|
|
1205
|
+
* {
|
|
1206
|
+
* path: 'login',
|
|
1207
|
+
* component: LoginComponent,
|
|
1208
|
+
* canActivate: [authGuard(AuthGuardCondition.SIGNED_IN, false, '/dashboard')]
|
|
1209
|
+
* }
|
|
1210
|
+
* ];
|
|
1211
|
+
* ```
|
|
1212
|
+
*
|
|
1213
|
+
* @see {@link AuthState} Service that provides authentication state information
|
|
1214
|
+
* @see {@link AuthGuardOption} Interface for configuring guard options
|
|
1215
|
+
* @see {@link AuthGuardCondition} Enum defining available authentication conditions
|
|
1216
|
+
* @see {@link getAllAuthGuardOptions} Utility function for extracting guard options from routes
|
|
1217
|
+
* @see {@link AUTH_GUARD_OPTIONS_KEY} Constant for storing guard options in route data
|
|
1218
|
+
*/
|
|
1219
|
+
function authGuard(param, state, redirect) {
|
|
1220
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1221
|
+
return async (route, _state) => {
|
|
1222
|
+
const router = inject(Router);
|
|
1223
|
+
const authState = inject(AuthState);
|
|
1224
|
+
route.data = {
|
|
1225
|
+
...route.data,
|
|
1226
|
+
[AUTH_GUARD_OPTIONS_KEY]: Array.isArray(param)
|
|
1227
|
+
? param
|
|
1228
|
+
: [{ condition: param, state: state, redirect: redirect }],
|
|
1229
|
+
};
|
|
1230
|
+
const conditionCheckers = {
|
|
1231
|
+
[AuthGuardCondition.SIGNED_IN]: authState.signedIn,
|
|
1232
|
+
[AuthGuardCondition.HAS_TOKEN]: authState.hasAccessToken,
|
|
1233
|
+
};
|
|
1234
|
+
const authGuardOptions = getAllAuthGuardOptions(route);
|
|
1235
|
+
if (!authGuardOptions.length) {
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
const conditionsMet = authGuardOptions.every(option => {
|
|
1239
|
+
return option.state === conditionCheckers[option.condition]();
|
|
1240
|
+
});
|
|
1241
|
+
if (!conditionsMet) {
|
|
1242
|
+
const option = authGuardOptions.pop();
|
|
1243
|
+
const redirectPath = option.redirect.startsWith("/") ? option.redirect : `/${option.redirect}`;
|
|
1244
|
+
await router.navigateByUrl(redirectPath);
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
return true;
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// noinspection JSUnusedGlobalSymbols
|
|
1252
|
+
function roleGuard(param, state, redirect) {
|
|
1253
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1254
|
+
return async (route, _state) => {
|
|
1255
|
+
const router = inject(Router);
|
|
1256
|
+
const authState = inject(AuthState);
|
|
1257
|
+
route.data = {
|
|
1258
|
+
...route.data,
|
|
1259
|
+
[AUTH_GUARD_OPTIONS_KEY]: Array.isArray(param)
|
|
1260
|
+
? param
|
|
1261
|
+
: [{ condition: param, state: state, redirect: redirect }],
|
|
1262
|
+
};
|
|
1263
|
+
const conditionCheckers = {
|
|
1264
|
+
[AuthGuardCondition.SIGNED_IN]: authState.signedIn,
|
|
1265
|
+
[AuthGuardCondition.HAS_TOKEN]: authState.hasAccessToken,
|
|
1266
|
+
};
|
|
1267
|
+
const authGuardOptions = getAllAuthGuardOptions(route);
|
|
1268
|
+
if (!authGuardOptions.length) {
|
|
1269
|
+
return true;
|
|
1270
|
+
}
|
|
1271
|
+
const conditionsMet = authGuardOptions.every(option => {
|
|
1272
|
+
return option.state === conditionCheckers[option.condition]();
|
|
1273
|
+
});
|
|
1274
|
+
if (!conditionsMet) {
|
|
1275
|
+
const option = authGuardOptions.pop();
|
|
1276
|
+
const redirectPath = option.redirect.startsWith("/") ? option.redirect : `/${option.redirect}`;
|
|
1277
|
+
await router.navigateByUrl(redirectPath);
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
return true;
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Array of authentication error codes that should trigger token refresh instead of immediate redirect
|
|
1286
|
+
*
|
|
1287
|
+
* These error codes indicate authentication issues that can potentially be resolved
|
|
1288
|
+
* by refreshing the access token. When these errors are encountered, the interceptor
|
|
1289
|
+
* will attempt to refresh the token before redirecting the user to the login page.
|
|
1290
|
+
*
|
|
1291
|
+
* @example
|
|
1292
|
+
* ```typescript
|
|
1293
|
+
* // The interceptor checks if the error code is in this array
|
|
1294
|
+
* if (SKIPPED_ERRORS.includes(error.error?.code)) {
|
|
1295
|
+
* // Attempt token refresh instead of immediate redirect
|
|
1296
|
+
* return refreshToken(req, next);
|
|
1297
|
+
* }
|
|
1298
|
+
* ```
|
|
1299
|
+
*
|
|
1300
|
+
* @see {@link AuthErrorResponseCode} Enum containing all authentication error codes
|
|
1301
|
+
* @see {@link authInterceptor} Function that uses this array for error handling
|
|
1302
|
+
*/
|
|
1303
|
+
const SKIPPED_ERRORS = [
|
|
1304
|
+
AuthErrorResponseCode.AUTH_401_EXPIRED_TOKEN,
|
|
1305
|
+
AuthErrorResponseCode.AUTH_401_INVALID_TOKEN,
|
|
1306
|
+
AuthErrorResponseCode.AUTH_401_NOT_LOGGED_IN,
|
|
1307
|
+
];
|
|
1308
|
+
/**
|
|
1309
|
+
* Flag to prevent multiple simultaneous token refresh operations
|
|
1310
|
+
*
|
|
1311
|
+
* This global flag ensures that only one token refresh operation can be in progress
|
|
1312
|
+
* at any given time. This prevents race conditions and duplicate refresh requests
|
|
1313
|
+
* when multiple HTTP requests fail simultaneously due to expired tokens.
|
|
1314
|
+
*
|
|
1315
|
+
* @private
|
|
1316
|
+
*/
|
|
1317
|
+
let refreshingInProgress = false;
|
|
1318
|
+
/**
|
|
1319
|
+
* Checks if an HTTP request is a token refresh request
|
|
1320
|
+
*
|
|
1321
|
+
* This utility function determines whether the given HTTP request is attempting
|
|
1322
|
+
* to refresh authentication tokens. This is important to avoid intercepting
|
|
1323
|
+
* and modifying token refresh requests themselves, which could cause infinite loops.
|
|
1324
|
+
*
|
|
1325
|
+
* @param req - The HTTP request to check
|
|
1326
|
+
* @returns True if the request is a token refresh request, false otherwise
|
|
1327
|
+
*
|
|
1328
|
+
* @example
|
|
1329
|
+
* ```typescript
|
|
1330
|
+
* // Used in the interceptor to avoid processing refresh token requests
|
|
1331
|
+
* if (!isRefreshTokenReq(req)) {
|
|
1332
|
+
* // Apply authentication logic
|
|
1333
|
+
* }
|
|
1334
|
+
* ```
|
|
1335
|
+
*
|
|
1336
|
+
* @private
|
|
1337
|
+
* @see {@link AuthEndpoint.REFRESH_TOKEN} Endpoint constant for token refresh
|
|
1338
|
+
* @see {@link Endpoint.AUTH} Base authentication endpoint
|
|
1339
|
+
*/
|
|
1340
|
+
const isRefreshTokenReq = (req) => req.url.includes(`${Endpoint.AUTH}/${AuthEndpoint.REFRESH_TOKEN}`);
|
|
1341
|
+
/**
|
|
1342
|
+
* Subject for coordinating token refresh operations across multiple HTTP requests
|
|
1343
|
+
*
|
|
1344
|
+
* This ReplaySubject is used to coordinate token refresh operations when multiple
|
|
1345
|
+
* HTTP requests fail simultaneously due to expired tokens. It ensures that all
|
|
1346
|
+
* waiting requests receive the new token once the refresh operation completes.
|
|
1347
|
+
*
|
|
1348
|
+
* The subject emits the new access token when refresh succeeds, or an error when
|
|
1349
|
+
* refresh fails. It uses ReplaySubject to ensure late subscribers still receive
|
|
1350
|
+
* the last emitted value.
|
|
1351
|
+
*
|
|
1352
|
+
* @private
|
|
1353
|
+
* @see {@link ReplaySubject} RxJS subject type used for token coordination
|
|
1354
|
+
* @see {@link AccessToken} Type representing access tokens
|
|
1355
|
+
*/
|
|
1356
|
+
let tokenSubject = new ReplaySubject(1);
|
|
1357
|
+
/**
|
|
1358
|
+
* Creates an HTTP interceptor for handling authentication tokens and automatic token refresh
|
|
1359
|
+
*
|
|
1360
|
+
* This interceptor automatically adds authentication tokens to outgoing HTTP requests
|
|
1361
|
+
* and handles token refresh when requests fail due to expired tokens. It provides
|
|
1362
|
+
* seamless authentication management for Angular applications using JWT tokens.
|
|
1363
|
+
*
|
|
1364
|
+
* Key features:
|
|
1365
|
+
* - Automatic token attachment to HTTP requests
|
|
1366
|
+
* - Automatic token refresh on authentication errors
|
|
1367
|
+
* - Prevention of multiple simultaneous refresh operations
|
|
1368
|
+
* - Coordinated handling of multiple failed requests during token refresh
|
|
1369
|
+
* - Automatic redirect to login page when refresh fails
|
|
1370
|
+
* - Configurable redirect behavior with optional callback
|
|
1371
|
+
*
|
|
1372
|
+
* The interceptor works by:
|
|
1373
|
+
* 1. Adding the current access token to outgoing requests
|
|
1374
|
+
* 2. Monitoring responses for authentication errors
|
|
1375
|
+
* 3. Attempting token refresh when authentication errors occur
|
|
1376
|
+
* 4. Retrying failed requests with the new token
|
|
1377
|
+
* 5. Redirecting to login when refresh fails or no refresh token is available
|
|
1378
|
+
*
|
|
1379
|
+
* @param redirect - The path to redirect to when authentication fails completely
|
|
1380
|
+
* @param onRedirect - Optional callback function to execute before redirecting
|
|
1381
|
+
* @returns An HttpInterceptorFn that can be used in Angular HTTP interceptor configuration
|
|
1382
|
+
*
|
|
1383
|
+
* @example
|
|
1384
|
+
* ```typescript
|
|
1385
|
+
* // Basic usage in app configuration
|
|
1386
|
+
* export const appConfig: ApplicationConfig = {
|
|
1387
|
+
* providers: [
|
|
1388
|
+
* provideHttpClient(
|
|
1389
|
+
* withInterceptors([
|
|
1390
|
+
* authInterceptor('/login')
|
|
1391
|
+
* ])
|
|
1392
|
+
* )
|
|
1393
|
+
* ]
|
|
1394
|
+
* };
|
|
1395
|
+
* ```
|
|
1396
|
+
*
|
|
1397
|
+
* @example
|
|
1398
|
+
* ```typescript
|
|
1399
|
+
* // With custom redirect callback
|
|
1400
|
+
* export const appConfig: ApplicationConfig = {
|
|
1401
|
+
* providers: [
|
|
1402
|
+
* provideHttpClient(
|
|
1403
|
+
* withInterceptors([
|
|
1404
|
+
* authInterceptor('/login', () => {
|
|
1405
|
+
* console.log('Redirecting to login due to authentication failure');
|
|
1406
|
+
* // Clear any cached data
|
|
1407
|
+
* localStorage.clear();
|
|
1408
|
+
* })
|
|
1409
|
+
* ])
|
|
1410
|
+
* )
|
|
1411
|
+
* ]
|
|
1412
|
+
* };
|
|
1413
|
+
* ```
|
|
1414
|
+
*
|
|
1415
|
+
* @example
|
|
1416
|
+
* ```typescript
|
|
1417
|
+
* // In a module-based application
|
|
1418
|
+
* @NgModule({
|
|
1419
|
+
* providers: [
|
|
1420
|
+
* {
|
|
1421
|
+
* provide: HTTP_INTERCEPTORS,
|
|
1422
|
+
* useValue: authInterceptor('/auth/login'),
|
|
1423
|
+
* multi: true
|
|
1424
|
+
* }
|
|
1425
|
+
* ]
|
|
1426
|
+
* })
|
|
1427
|
+
* export class AppModule {}
|
|
1428
|
+
* ```
|
|
1429
|
+
*
|
|
1430
|
+
* @see {@link AuthState} Service that provides authentication state and tokens
|
|
1431
|
+
* @see {@link AuthService} Service that handles token refresh operations
|
|
1432
|
+
* @see {@link HttpInterceptorFn} Angular HTTP interceptor function type
|
|
1433
|
+
* @see {@link SKIPPED_ERRORS} Array of error codes that trigger token refresh
|
|
1434
|
+
*/
|
|
1435
|
+
function authInterceptor(redirect, onRedirect) {
|
|
1436
|
+
return (req, next) => {
|
|
1437
|
+
const authState = inject(AuthState);
|
|
1438
|
+
const authService = inject(AuthService);
|
|
1439
|
+
const router = inject(Router);
|
|
1440
|
+
const setAccessToken = (req, accessToken) => {
|
|
1441
|
+
return req.clone({
|
|
1442
|
+
headers: req.headers.set("Authorization", `Bearer ${accessToken}`),
|
|
1443
|
+
});
|
|
1444
|
+
};
|
|
1445
|
+
const gotoSignIn = () => {
|
|
1446
|
+
onRedirect?.();
|
|
1447
|
+
authState.reset();
|
|
1448
|
+
// eslint-disable-next-line no-void
|
|
1449
|
+
void router.navigateByUrl(redirect);
|
|
1450
|
+
};
|
|
1451
|
+
const refreshToken = (req, next) => {
|
|
1452
|
+
if (!refreshingInProgress) {
|
|
1453
|
+
refreshingInProgress = true;
|
|
1454
|
+
tokenSubject.next(null);
|
|
1455
|
+
const refreshToken = authState.refreshToken();
|
|
1456
|
+
if (!refreshToken) {
|
|
1457
|
+
refreshingInProgress = false;
|
|
1458
|
+
gotoSignIn();
|
|
1459
|
+
return throwError(() => new Error("Refresh token not found."));
|
|
1460
|
+
}
|
|
1461
|
+
return authService.refreshToken(refreshToken).pipe(switchMap((tokenResponse) => {
|
|
1462
|
+
authState.setTokens(tokenResponse);
|
|
1463
|
+
tokenSubject.next(tokenResponse.accessToken);
|
|
1464
|
+
tokenSubject.complete();
|
|
1465
|
+
tokenSubject = new ReplaySubject(1);
|
|
1466
|
+
refreshingInProgress = false;
|
|
1467
|
+
return next(setAccessToken(req, tokenResponse.accessToken));
|
|
1468
|
+
}), catchError((error) => {
|
|
1469
|
+
refreshingInProgress = false;
|
|
1470
|
+
tokenSubject.error(error);
|
|
1471
|
+
tokenSubject.complete();
|
|
1472
|
+
tokenSubject = new ReplaySubject(1);
|
|
1473
|
+
gotoSignIn();
|
|
1474
|
+
return throwError(() => error);
|
|
1475
|
+
}));
|
|
1476
|
+
}
|
|
1477
|
+
return tokenSubject.pipe(filter(result => result !== null), take(1), switchMap(token => {
|
|
1478
|
+
return next(setAccessToken(req, token));
|
|
1479
|
+
}));
|
|
1480
|
+
};
|
|
1481
|
+
const handleRequest = (req, next) => {
|
|
1482
|
+
return next(req).pipe(catchError((error) => {
|
|
1483
|
+
if (error.status === HttpClientErrorStatus.UNAUTHORIZED &&
|
|
1484
|
+
error.error?.code &&
|
|
1485
|
+
SKIPPED_ERRORS.includes(error.error?.code) &&
|
|
1486
|
+
!isRefreshTokenReq(req)) {
|
|
1487
|
+
if (authState.signedIn()) {
|
|
1488
|
+
return refreshToken(req, next);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return throwError(() => error);
|
|
1492
|
+
}));
|
|
1493
|
+
};
|
|
1494
|
+
if (authState.accessToken()) {
|
|
1495
|
+
const tokenizedRequest = req.clone({
|
|
1496
|
+
headers: req.headers.set("Authorization", `Bearer ${authState.accessToken()}`),
|
|
1497
|
+
});
|
|
1498
|
+
return handleRequest(tokenizedRequest, next);
|
|
1499
|
+
}
|
|
1500
|
+
return handleRequest(req, next);
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* Angular module for authentication functionality
|
|
1506
|
+
*
|
|
1507
|
+
* This module provides comprehensive authentication features for Angular applications,
|
|
1508
|
+
* including authentication forms, permission-based directives, and authentication services.
|
|
1509
|
+
* It integrates with the Hichchi authentication system and provides both components
|
|
1510
|
+
* and directives for building secure Angular applications.
|
|
1511
|
+
*
|
|
1512
|
+
* The module exports:
|
|
1513
|
+
* - AuthFormComponent: A ready-to-use authentication form component
|
|
1514
|
+
* - PermissionDirective: A structural directive for permission-based conditional rendering
|
|
1515
|
+
*
|
|
1516
|
+
* The module must be configured using the forRoot() method to provide the necessary
|
|
1517
|
+
* authentication configuration.
|
|
1518
|
+
*
|
|
1519
|
+
* @example
|
|
1520
|
+
* ```typescript
|
|
1521
|
+
* // Basic module configuration
|
|
1522
|
+
* @NgModule({
|
|
1523
|
+
* imports: [
|
|
1524
|
+
* NgxHichchiAuthModule.forRoot({
|
|
1525
|
+
* apiBaseURL: 'https://api.example.com'
|
|
1526
|
+
* })
|
|
1527
|
+
* ]
|
|
1528
|
+
* })
|
|
1529
|
+
* export class AppModule { }
|
|
1530
|
+
* ```
|
|
1531
|
+
*
|
|
1532
|
+
* @example
|
|
1533
|
+
* ```typescript
|
|
1534
|
+
* // Advanced configuration with custom authentication field
|
|
1535
|
+
* @NgModule({
|
|
1536
|
+
* imports: [
|
|
1537
|
+
* NgxHichchiAuthModule.forRoot({
|
|
1538
|
+
* apiBaseURL: 'https://api.example.com',
|
|
1539
|
+
* authField: AuthField.EMAIL
|
|
1540
|
+
* })
|
|
1541
|
+
* ]
|
|
1542
|
+
* })
|
|
1543
|
+
* export class AppModule { }
|
|
1544
|
+
* ```
|
|
1545
|
+
*
|
|
1546
|
+
* @see {@link AuthConfig} Configuration interface for the authentication module
|
|
1547
|
+
* @see {@link AuthFormComponent} Authentication form component
|
|
1548
|
+
* @see {@link PermissionDirective} Permission-based conditional rendering directive
|
|
1549
|
+
* @see {@link AuthService} Authentication service for managing user sessions
|
|
1550
|
+
*/
|
|
1551
|
+
class NgxHichchiAuthModule {
|
|
1552
|
+
/**
|
|
1553
|
+
* Configures the NgxHichchiAuthModule with the provided authentication configuration
|
|
1554
|
+
*
|
|
1555
|
+
* This static method sets up the module with the necessary providers and configuration
|
|
1556
|
+
* for authentication functionality. It provides the AuthService, HTTP client, and
|
|
1557
|
+
* authentication configuration token that are required for the module to function properly.
|
|
1558
|
+
*
|
|
1559
|
+
* @param config - The authentication configuration object containing API endpoints and settings
|
|
1560
|
+
* @returns A ModuleWithProviders object configured with authentication providers
|
|
1561
|
+
*
|
|
1562
|
+
* @example
|
|
1563
|
+
* ```typescript
|
|
1564
|
+
* // Basic configuration
|
|
1565
|
+
* NgxHichchiAuthModule.forRoot({
|
|
1566
|
+
* apiBaseURL: 'https://api.example.com'
|
|
1567
|
+
* })
|
|
1568
|
+
* ```
|
|
1569
|
+
*
|
|
1570
|
+
* @example
|
|
1571
|
+
* ```typescript
|
|
1572
|
+
* // Configuration with environment variables and authentication field
|
|
1573
|
+
* NgxHichchiAuthModule.forRoot({
|
|
1574
|
+
* apiBaseURL: environment.apiUrl,
|
|
1575
|
+
* authField: AuthField.USERNAME
|
|
1576
|
+
* })
|
|
1577
|
+
* ```
|
|
1578
|
+
*
|
|
1579
|
+
* @see {@link AuthConfig} Interface defining the configuration structure
|
|
1580
|
+
* @see {@link AUTH_CONFIG} Injection token for the authentication configuration
|
|
1581
|
+
* @see {@link AuthService} Service that uses the provided configuration
|
|
1582
|
+
*/
|
|
1583
|
+
static forRoot(config) {
|
|
1584
|
+
return {
|
|
1585
|
+
ngModule: NgxHichchiAuthModule,
|
|
1586
|
+
providers: [{ provide: AUTH_CONFIG, useValue: config }, provideHttpClient(), AuthService],
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: NgxHichchiAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
1590
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.7", ngImport: i0, type: NgxHichchiAuthModule, declarations: [AuthFormComponent], imports: [CommonModule,
|
|
1591
|
+
FormsModule,
|
|
1592
|
+
ReactiveFormsModule,
|
|
1593
|
+
ButtonComponent,
|
|
1594
|
+
HcCardComponent,
|
|
1595
|
+
HcCardComponent,
|
|
1596
|
+
HcSeparatorComponent,
|
|
1597
|
+
PermissionDirective], exports: [AuthFormComponent, PermissionDirective] });
|
|
1598
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: NgxHichchiAuthModule, imports: [CommonModule,
|
|
1599
|
+
FormsModule,
|
|
1600
|
+
ReactiveFormsModule,
|
|
1601
|
+
ButtonComponent,
|
|
1602
|
+
HcCardComponent,
|
|
1603
|
+
HcCardComponent,
|
|
1604
|
+
HcSeparatorComponent] });
|
|
1605
|
+
}
|
|
1606
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: NgxHichchiAuthModule, decorators: [{
|
|
1607
|
+
type: NgModule,
|
|
1608
|
+
args: [{
|
|
1609
|
+
declarations: [AuthFormComponent],
|
|
1610
|
+
imports: [
|
|
1611
|
+
CommonModule,
|
|
1612
|
+
FormsModule,
|
|
1613
|
+
ReactiveFormsModule,
|
|
1614
|
+
ButtonComponent,
|
|
1615
|
+
HcCardComponent,
|
|
1616
|
+
HcCardComponent,
|
|
1617
|
+
HcSeparatorComponent,
|
|
1618
|
+
PermissionDirective,
|
|
1619
|
+
],
|
|
1620
|
+
exports: [AuthFormComponent, PermissionDirective],
|
|
1621
|
+
}]
|
|
1622
|
+
}] });
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Generated bundle index. Do not edit.
|
|
1626
|
+
*/
|
|
1627
|
+
|
|
1628
|
+
export { AuthFormComponent, AuthGuardCondition, AuthService, AuthState, NgxHichchiAuthModule, PermissionDirective, SKIPPED_ERRORS, authGuard, authInterceptor, roleGuard };
|
|
1629
|
+
//# sourceMappingURL=hichchi-ngx-auth.mjs.map
|