mesauth-angular 1.2.0 → 1.2.1

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.
@@ -1,406 +0,0 @@
1
- import { inject, Injectable, InjectionToken, EnvironmentProviders, makeEnvironmentProviders, provideAppInitializer } from '@angular/core';
2
- import { HttpClient } from '@angular/common/http';
3
- import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
4
- import { BehaviorSubject, Subject, Observable, of, EMPTY } from 'rxjs';
5
- import { tap, catchError } from 'rxjs/operators';
6
- import { Router } from '@angular/router';
7
-
8
- export interface MesAuthConfig {
9
- apiBaseUrl: string;
10
- withCredentials?: boolean;
11
- userBaseUrl?: string;
12
- }
13
-
14
- /** Injection token for MesAuth configuration */
15
- export const MES_AUTH_CONFIG = new InjectionToken<MesAuthConfig>('MES_AUTH_CONFIG');
16
-
17
- /**
18
- * Provides MesAuth with configuration.
19
- * This is the recommended way to set up mesauth-angular in standalone apps.
20
- *
21
- * @example
22
- * ```typescript
23
- * // app.config.ts
24
- * export const appConfig: ApplicationConfig = {
25
- * providers: [
26
- * provideHttpClient(withInterceptors([mesAuthInterceptor])),
27
- * provideMesAuth({
28
- * apiBaseUrl: 'https://auth.example.com',
29
- * userBaseUrl: 'https://app.example.com'
30
- * })
31
- * ]
32
- * };
33
- * ```
34
- */
35
- export function provideMesAuth(config: MesAuthConfig): EnvironmentProviders {
36
- return makeEnvironmentProviders([
37
- { provide: MES_AUTH_CONFIG, useValue: config },
38
- MesAuthService,
39
- provideAppInitializer(() => {
40
- const mesAuthService = inject(MesAuthService);
41
- const httpClient = inject(HttpClient);
42
- const router = inject(Router);
43
- mesAuthService.init(config, httpClient, router);
44
- })
45
- ]);
46
- }
47
-
48
- export interface IUser {
49
- userId?: string;
50
- userName?: string;
51
- fullName?: string;
52
- gender?: string;
53
- email?: string;
54
- phoneNumber?: string;
55
- department?: string;
56
- position?: string;
57
- tokenVersion?: string;
58
- permEndpoint?: string;
59
- perms?: Set<string>;
60
- employeeCode?: string;
61
- avatarPath?: string;
62
- loginMethod?: number;
63
- hrFullNameVn?: string;
64
- hrFullNameEn?: string;
65
- hrPosition?: string;
66
- hrJobTitle?: string;
67
- hrGender?: string;
68
- hrMobile?: string;
69
- hrEmail?: string;
70
- hrJoinDate?: string;
71
- hrBirthDate?: string;
72
- hrWorkStatus?: string;
73
- hrDoiTuong?: string;
74
- hrTeamCode?: string;
75
- hrLineCode?: string;
76
- }
77
-
78
- export enum NotificationType {
79
- Info = 'Info',
80
- Warning = 'Warning',
81
- Error = 'Error',
82
- Success = 'Success'
83
- }
84
-
85
- export interface NotificationDto {
86
- id: string;
87
- title: string;
88
- message: string;
89
- messageHtml?: string;
90
- url?: string;
91
- type: NotificationType;
92
- isRead: boolean;
93
- createdAt: string;
94
- sourceAppName: string;
95
- sourceAppIconUrl?: string;
96
- }
97
-
98
- export interface FrontEndRoute {
99
- id: number;
100
- roleId: string;
101
- roleName: string;
102
- routePath: string;
103
- routeName: string;
104
- description?: string;
105
- icon?: string;
106
- cssClass?: string;
107
- parentId?: number | null;
108
- sortOrder: number;
109
- isLabel: boolean;
110
- isActive: boolean;
111
- createdAt: string;
112
- updatedAt?: string;
113
- children: FrontEndRoute[];
114
- }
115
-
116
- export interface UserFrontEndRoutesGrouped {
117
- appId: string;
118
- appName: string;
119
- feUrl?: string;
120
- routes: FrontEndRoute[];
121
- }
122
-
123
- export interface FrontEndRouteMaster {
124
- id: number;
125
- appId: string;
126
- routePath: string;
127
- routeName: string;
128
- description?: string;
129
- icon?: string;
130
- cssClass?: string;
131
- parentId?: number | null;
132
- sortOrder: number;
133
- isLabel: boolean;
134
- isActive: boolean;
135
- createdAt: string;
136
- updatedAt?: string;
137
- }
138
-
139
- export interface CreateFrontEndRouteDto {
140
- routePath: string;
141
- routeName: string;
142
- description?: string;
143
- icon?: string;
144
- cssClass?: string;
145
- parentId?: number | null;
146
- sortOrder?: number;
147
- isLabel?: boolean;
148
- }
149
-
150
- export interface PagedList<T> {
151
- items: T[];
152
- totalCount: number;
153
- page: number;
154
- pageSize: number;
155
- totalPages: number;
156
- hasNext: boolean;
157
- hasPrevious: boolean;
158
- }
159
-
160
- export interface RealTimeNotificationDto {
161
- id: string;
162
- title: string;
163
- message: string;
164
- messageHtml?: string;
165
- url?: string;
166
- type: NotificationType;
167
- createdAt: string;
168
- sourceAppName: string;
169
- sourceAppIconUrl?: string;
170
- }
171
-
172
- @Injectable()
173
- export class MesAuthService {
174
- private hubConnection: HubConnection | null = null;
175
- private _currentUser = new BehaviorSubject<IUser | null>(null);
176
- public currentUser$: Observable<IUser | null> = this._currentUser.asObservable();
177
- private _notifications = new Subject<any>();
178
- public notifications$: Observable<any> = this._notifications.asObservable();
179
-
180
- private apiBase = '';
181
- private config: MesAuthConfig | null = null;
182
- private http!: HttpClient;
183
- private router?: Router;
184
-
185
- constructor() {
186
- // Empty constructor - all dependencies passed to init()
187
- }
188
-
189
- init(config: MesAuthConfig, httpClient: HttpClient, router?: Router) {
190
- this.config = config;
191
- this.http = httpClient;
192
- this.router = router;
193
- this.apiBase = config.apiBaseUrl.replace(/\/$/, '');
194
-
195
- // Fetch user once on init. Route changes do NOT re-fetch the user.
196
- // Auth state is maintained via cookies; 401 errors are handled by HTTP interceptors.
197
- // SignalR handles real-time notification delivery without polling.
198
- this.fetchCurrentUser().subscribe();
199
- }
200
-
201
- getConfig(): MesAuthConfig | null {
202
- return this.config;
203
- }
204
-
205
- private fetchCurrentUser(): Observable<any> {
206
- if (!this.apiBase) return EMPTY;
207
- const url = `${this.apiBase}/auth/me`;
208
- return this.http.get(url, { withCredentials: this.config?.withCredentials ?? true }).pipe(
209
- tap((u) => {
210
- this._currentUser.next(u);
211
- if (u && this.config) {
212
- this.startConnection(this.config);
213
- }
214
- }),
215
- catchError((err) => {
216
- // Silently handle auth errors (401/403) - user is not logged in
217
- if (err.status === 401 || err.status === 403) {
218
- this._currentUser.next(null);
219
- }
220
- return of(null);
221
- })
222
- );
223
- }
224
-
225
- public getUnreadCount(): Observable<any> {
226
- return this.http.get(`${this.apiBase}/notif/me/unread-count`, { withCredentials: this.config?.withCredentials ?? true });
227
- }
228
-
229
- public getNotifications(page: number = 1, pageSize: number = 20, includeRead: boolean = false, type?: string): Observable<any> {
230
- let url = `${this.apiBase}/notif/me?page=${page}&pageSize=${pageSize}&includeRead=${includeRead}`;
231
- if (type) {
232
- url += `&type=${type}`;
233
- }
234
- return this.http.get(url, { withCredentials: this.config?.withCredentials ?? true });
235
- }
236
-
237
- public markAsRead(notificationId: string): Observable<any> {
238
- return this.http.patch(`${this.apiBase}/notif/${notificationId}/read`, {}, { withCredentials: this.config?.withCredentials ?? true });
239
- }
240
-
241
- public markAllAsRead(): Observable<any> {
242
- return this.http.patch(`${this.apiBase}/notif/me/read-all`, {}, { withCredentials: this.config?.withCredentials ?? true });
243
- }
244
-
245
- public deleteNotification(notificationId: string): Observable<any> {
246
- return this.http.delete(`${this.apiBase}/notif/${notificationId}`, { withCredentials: this.config?.withCredentials ?? true });
247
- }
248
-
249
- /**
250
- * Get frontend routes assigned to the current user
251
- * Returns routes grouped by application
252
- */
253
- public getFrontEndRoutes(): Observable<UserFrontEndRoutesGrouped[]> {
254
- if (!this.apiBase) throw new Error('MesAuth not initialized');
255
- return this.http.get<UserFrontEndRoutesGrouped[]>(`${this.apiBase}/fe-routes/me`, { withCredentials: this.config?.withCredentials ?? true });
256
- }
257
-
258
- /**
259
- * Get master routes for a specific application
260
- * @param appId - The application ID
261
- */
262
- public getRouteMasters(appId: string): Observable<FrontEndRouteMaster[]> {
263
- if (!this.apiBase) throw new Error('MesAuth not initialized');
264
- return this.http.get<FrontEndRouteMaster[]>(`${this.apiBase}/fe-routes/masters/${appId}`, { withCredentials: this.config?.withCredentials ?? true });
265
- }
266
-
267
- /**
268
- * Register/sync frontend routes for an application
269
- * This is typically called on app startup to sync routes from the frontend app
270
- * @param appId - The application ID (passed via X-App-Id header)
271
- * @param routes - Array of route definitions
272
- */
273
- public registerFrontEndRoutes(appId: string, routes: CreateFrontEndRouteDto[]): Observable<any> {
274
- if (!this.apiBase) throw new Error('MesAuth not initialized');
275
- const headers = { 'X-App-Id': appId };
276
- return this.http.post(`${this.apiBase}/fe-routes/register`, routes, {
277
- headers,
278
- withCredentials: this.config?.withCredentials ?? true
279
- });
280
- }
281
-
282
- /**
283
- * Create a new route master
284
- * @param appId - The application ID
285
- * @param route - Route details
286
- */
287
- public createRouteMaster(appId: string, route: CreateFrontEndRouteDto): Observable<FrontEndRouteMaster> {
288
- if (!this.apiBase) throw new Error('MesAuth not initialized');
289
- return this.http.post<FrontEndRouteMaster>(`${this.apiBase}/fe-routes/masters`, {
290
- appId,
291
- ...route
292
- }, { withCredentials: this.config?.withCredentials ?? true });
293
- }
294
-
295
- /**
296
- * Update an existing route master
297
- * @param routeId - The route master ID
298
- * @param route - Updated route details
299
- */
300
- public updateRouteMaster(routeId: number, route: Partial<CreateFrontEndRouteDto> & { isActive?: boolean }): Observable<FrontEndRouteMaster> {
301
- if (!this.apiBase) throw new Error('MesAuth not initialized');
302
- return this.http.put<FrontEndRouteMaster>(`${this.apiBase}/fe-routes/masters/${routeId}`, route, {
303
- withCredentials: this.config?.withCredentials ?? true
304
- });
305
- }
306
-
307
- /**
308
- * Delete a route master
309
- * @param routeId - The route master ID
310
- */
311
- public deleteRouteMaster(routeId: number): Observable<any> {
312
- if (!this.apiBase) throw new Error('MesAuth not initialized');
313
- return this.http.delete(`${this.apiBase}/fe-routes/masters/${routeId}`, {
314
- withCredentials: this.config?.withCredentials ?? true
315
- });
316
- }
317
-
318
- /**
319
- * Assign a route to a role
320
- * @param routeMasterId - The route master ID
321
- * @param roleId - The role ID (GUID)
322
- */
323
- public assignRouteToRole(routeMasterId: number, roleId: string): Observable<any> {
324
- if (!this.apiBase) throw new Error('MesAuth not initialized');
325
- return this.http.post(`${this.apiBase}/fe-routes/mappings`, {
326
- routeMasterId,
327
- roleId
328
- }, { withCredentials: this.config?.withCredentials ?? true });
329
- }
330
-
331
- /**
332
- * Remove a route assignment from a role
333
- * @param mappingId - The mapping ID
334
- */
335
- public removeRouteFromRole(mappingId: number): Observable<any> {
336
- if (!this.apiBase) throw new Error('MesAuth not initialized');
337
- return this.http.delete(`${this.apiBase}/fe-routes/mappings/${mappingId}`, {
338
- withCredentials: this.config?.withCredentials ?? true
339
- });
340
- }
341
-
342
- /**
343
- * Get route-to-role mappings for a specific role
344
- * @param roleId - The role ID (GUID)
345
- */
346
- public getRouteMappingsByRole(roleId: string): Observable<any[]> {
347
- if (!this.apiBase) throw new Error('MesAuth not initialized');
348
- return this.http.get<any[]>(`${this.apiBase}/fe-routes/mappings?roleId=${roleId}`, {
349
- withCredentials: this.config?.withCredentials ?? true
350
- });
351
- }
352
-
353
- private startConnection(config: MesAuthConfig) {
354
- if (this.hubConnection) return;
355
- const signalrUrl = config.apiBaseUrl.replace(/\/$/, '') + '/hub/notification';
356
- const builder = new HubConnectionBuilder()
357
- .withUrl(signalrUrl, { withCredentials: config.withCredentials ?? true })
358
- .withAutomaticReconnect()
359
- .configureLogging(LogLevel.Warning);
360
-
361
- this.hubConnection = builder.build();
362
-
363
- this.hubConnection.on('ReceiveNotification', (n: any) => {
364
- this._notifications.next(n);
365
- });
366
-
367
- this.hubConnection.start().then(() => {}).catch((err) => {});
368
-
369
- this.hubConnection.onclose(() => {});
370
- this.hubConnection.onreconnecting(() => {});
371
- this.hubConnection.onreconnected(() => {});
372
- }
373
-
374
- public stop() {
375
- if (!this.hubConnection) return;
376
- this.hubConnection.stop().catch(() => {});
377
- this.hubConnection = null;
378
- }
379
-
380
- public logout(): Observable<any> {
381
- const url = `${this.apiBase}/auth/logout`;
382
- return this.http.post(url, {}, { withCredentials: this.config?.withCredentials ?? true }).pipe(
383
- tap(() => {
384
- this._currentUser.next(null);
385
- this.stop();
386
- })
387
- );
388
- }
389
-
390
- public get currentUser(): IUser | null {
391
- return this._currentUser.value;
392
- }
393
-
394
- public get isAuthenticated(): boolean {
395
- return this._currentUser.value !== null;
396
- }
397
-
398
- /**
399
- * Refreshes the current user from the server.
400
- * Returns an Observable that completes when the user data is loaded.
401
- * Callers can subscribe to wait for completion before proceeding (e.g., navigating after login).
402
- */
403
- public refreshUser(): Observable<any> {
404
- return this.fetchCurrentUser();
405
- }
406
- }
@@ -1,125 +0,0 @@
1
- import { Component, OnInit, OnDestroy, Output, EventEmitter, HostBinding } from '@angular/core';
2
- import { NgIf } from '@angular/common';
3
- import { MesAuthService } from './mes-auth.service';
4
- import { ThemeService, Theme } from './theme.service';
5
- import { Subject } from 'rxjs';
6
- import { takeUntil } from 'rxjs/operators';
7
-
8
- @Component({
9
- selector: 'ma-notification-badge',
10
- standalone: true,
11
- imports: [NgIf],
12
- template: `
13
- <button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
14
- <span class="icon">🔔</span>
15
- <span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
16
- </button>
17
- `,
18
- styles: [`
19
- :host {
20
- --error-color: #f44336;
21
- }
22
-
23
- :host(.theme-dark) {
24
- --error-color: #ef5350;
25
- }
26
-
27
- .notification-btn {
28
- position: relative;
29
- background: none;
30
- border: none;
31
- font-size: 24px;
32
- cursor: pointer;
33
- padding: 8px;
34
- transition: opacity 0.2s;
35
- }
36
-
37
- .notification-btn:hover {
38
- opacity: 0.7;
39
- }
40
-
41
- .icon {
42
- display: inline-block;
43
- }
44
-
45
- .badge {
46
- position: absolute;
47
- top: 0;
48
- right: 0;
49
- background-color: var(--error-color);
50
- color: white;
51
- border-radius: 50%;
52
- width: 20px;
53
- height: 20px;
54
- display: flex;
55
- align-items: center;
56
- justify-content: center;
57
- font-size: 12px;
58
- font-weight: bold;
59
- }
60
- `]
61
- })
62
- export class NotificationBadgeComponent implements OnInit, OnDestroy {
63
- @Output() notificationClick = new EventEmitter<void>();
64
- @HostBinding('class') get themeClass(): string {
65
- return `theme-${this.currentTheme}`;
66
- }
67
-
68
- unreadCount = 0;
69
- currentTheme: Theme = 'light';
70
- private hasUser = false;
71
- private destroy$ = new Subject<void>();
72
-
73
- constructor(private authService: MesAuthService, private themeService: ThemeService) {}
74
-
75
- ngOnInit() {
76
- this.themeService.currentTheme$
77
- .pipe(takeUntil(this.destroy$))
78
- .subscribe(theme => {
79
- this.currentTheme = theme;
80
- });
81
-
82
- this.authService.currentUser$
83
- .pipe(takeUntil(this.destroy$))
84
- .subscribe(user => {
85
- this.hasUser = !!user;
86
- if (!this.hasUser) {
87
- this.unreadCount = 0;
88
- return;
89
- }
90
- this.loadUnreadCount();
91
- });
92
-
93
- // Listen for new notifications
94
- this.authService.notifications$
95
- .pipe(takeUntil(this.destroy$))
96
- .subscribe(() => {
97
- if (this.hasUser) {
98
- this.loadUnreadCount();
99
- }
100
- });
101
- }
102
-
103
- ngOnDestroy() {
104
- this.destroy$.next();
105
- this.destroy$.complete();
106
- }
107
-
108
- private loadUnreadCount() {
109
- if (!this.hasUser) {
110
- this.unreadCount = 0;
111
- return;
112
- }
113
-
114
- this.authService.getUnreadCount().subscribe({
115
- next: (response: any) => {
116
- this.unreadCount = response.unreadCount || 0;
117
- },
118
- error: (err) => console.error('Error loading unread count:', err)
119
- });
120
- }
121
-
122
- onNotificationClick() {
123
- this.notificationClick.emit();
124
- }
125
- }