@opendevstack/ngx-appshell 19.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/fesm2022/opendevstack-ngx-appshell.mjs +727 -0
- package/fesm2022/opendevstack-ngx-appshell.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/components/appshell-breadcrumb/appshell-breadcrumb.component.d.ts +7 -0
- package/lib/components/appshell-chip/appshell-chip.component.d.ts +6 -0
- package/lib/components/appshell-filters/appshell-filters.component.d.ts +12 -0
- package/lib/components/appshell-header/appshell-header.component.d.ts +24 -0
- package/lib/components/appshell-icon/appshell-icon.component.d.ts +12 -0
- package/lib/components/appshell-layout/appshell-layout.component.d.ts +25 -0
- package/lib/components/appshell-page-header/appshell-page-header.component.d.ts +18 -0
- package/lib/components/appshell-platform-header/appshell-platform-header.component.d.ts +37 -0
- package/lib/components/appshell-platform-layout/appshell-platform-layout.component.d.ts +30 -0
- package/lib/components/appshell-product-card/appshell-product-card.component.d.ts +23 -0
- package/lib/components/appshell-product-card-v2/appshell-product-card-v2.component.d.ts +16 -0
- package/lib/components/appshell-select/appshell-select.component.d.ts +9 -0
- package/lib/components/appshell-sidebar-menu/appshell-sidebar-menu.component.d.ts +12 -0
- package/lib/components/appshell-toast/appshell-toast.component.d.ts +9 -0
- package/lib/components/appshell-toasts/appshell-toasts.component.d.ts +14 -0
- package/lib/components/index.d.ts +15 -0
- package/lib/directives/appshell-link.directive.d.ts +15 -0
- package/lib/directives/index.d.ts +1 -0
- package/lib/models/appshell-button.d.ts +5 -0
- package/lib/models/appshell-filter.d.ts +4 -0
- package/lib/models/appshell-link.d.ts +6 -0
- package/lib/models/appshell-links-group.d.ts +5 -0
- package/lib/models/appshell-notification.d.ts +10 -0
- package/lib/models/appshell-picker.d.ts +9 -0
- package/lib/models/appshell-product.d.ts +12 -0
- package/lib/models/appshell-tag.d.ts +4 -0
- package/lib/models/appshell-toast.d.ts +6 -0
- package/lib/models/appshell-user.d.ts +5 -0
- package/lib/models/index.d.ts +10 -0
- package/lib/screens/appshell-notifications-screen/appshell-notifications-screen.component.d.ts +23 -0
- package/lib/screens/appshell-product-catalog-screen/appshell-product-catalog-screen.component.d.ts +16 -0
- package/lib/screens/appshell-product-view-screen/appshell-product-view-screen.component.d.ts +19 -0
- package/lib/screens/index.d.ts +3 -0
- package/lib/services/appshell-toast.service.d.ts +12 -0
- package/lib/services/icon-registry.service.d.ts +14 -0
- package/lib/services/index.d.ts +2 -0
- package/opendevstack-ngx-appshell-19.0.5.tgz +0 -0
- package/package.json +44 -0
- package/public-api.d.ts +5 -0
- package/schematics/azure-login/files/app-config/config.json.template +12 -0
- package/schematics/azure-login/files/app-config-service/app-config.service.spec.ts.template +48 -0
- package/schematics/azure-login/files/app-config-service/app-config.service.ts.template +39 -0
- package/schematics/azure-login/files/azure-config/azure.config.ts.template +94 -0
- package/schematics/azure-login/files/azure-service/azure.service.spec.ts.template +311 -0
- package/schematics/azure-login/files/azure-service/azure.service.ts.template +161 -0
- package/schematics/azure-login/index.d.ts +2 -0
- package/schematics/azure-login/index.js +325 -0
- package/schematics/azure-login/index.js.map +1 -0
- package/schematics/azure-login/schema.json +8 -0
- package/schematics/collection.json +19 -0
- package/schematics/nats-notifications/files/nats-service/nats.service.spec.ts.template +473 -0
- package/schematics/nats-notifications/files/nats-service/nats.service.ts.template +255 -0
- package/schematics/nats-notifications/files/notifications-screen/notifications-screen.component.html.template +7 -0
- package/schematics/nats-notifications/files/notifications-screen/notifications-screen.component.spec.ts.template +152 -0
- package/schematics/nats-notifications/files/notifications-screen/notifications-screen.component.ts.template +61 -0
- package/schematics/nats-notifications/index.d.ts +2 -0
- package/schematics/nats-notifications/index.js +502 -0
- package/schematics/nats-notifications/index.js.map +1 -0
- package/schematics/nats-notifications/schema.json +8 -0
- package/schematics/ng-add/files/_fonts.scss +85 -0
- package/schematics/ng-add/files/styles.scss +47 -0
- package/schematics/ng-add/index.d.ts +2 -0
- package/schematics/ng-add/index.js +442 -0
- package/schematics/ng-add/index.js.map +1 -0
- package/styles/appshell-typography-config.scss +19 -0
- package/styles/appshell.theme.scss +75 -0
- package/styles/palette.css +92 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
|
2
|
+
import { AzureService } from './azure.service';
|
|
3
|
+
import { MSAL_GUARD_CONFIG, MsalBroadcastService, MsalGuardConfiguration, MsalService } from "@azure/msal-angular";
|
|
4
|
+
import { BehaviorSubject, of, Subject } from 'rxjs';
|
|
5
|
+
import { AuthenticationResult, EventMessage, EventType, InteractionStatus, InteractionType, RedirectRequest } from '@azure/msal-browser';
|
|
6
|
+
import { Router } from '@angular/router';
|
|
7
|
+
|
|
8
|
+
const destroyingMethodName = '_destroying$';
|
|
9
|
+
const fakeToken = 'test-token';
|
|
10
|
+
|
|
11
|
+
describe('AzureService', () => {
|
|
12
|
+
let service: AzureService;
|
|
13
|
+
let msalService: jasmine.SpyObj<MsalService>;
|
|
14
|
+
const msalGuardConfig: jasmine.SpyObj<MsalGuardConfiguration> = jasmine.createSpyObj('MsalGuardConfiguration', ['authRequest', 'interactionType']);
|
|
15
|
+
let msalSubject$: Subject<EventMessage>;
|
|
16
|
+
let inProgress$: Subject<InteractionStatus>;
|
|
17
|
+
const msalInstanceSpy = jasmine.createSpyObj('instance', ['enableAccountStorageEvents', 'getAllAccounts', 'getActiveAccount', 'setActiveAccount', 'acquireTokenSilent']);
|
|
18
|
+
const mockRouter: jasmine.SpyObj<Router> = jasmine.createSpyObj('Router', ['navigate']);;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
msalSubject$ = new Subject<EventMessage>();
|
|
22
|
+
inProgress$ = new Subject<InteractionStatus>();
|
|
23
|
+
|
|
24
|
+
const msalServiceSpy = jasmine.createSpyObj('MsalService', ['handleRedirectObservable', 'instance', 'loginRedirect', 'logout']);
|
|
25
|
+
const msalBroadcastServiceSpy = jasmine.createSpyObj('MsalBroadcastService', [], {
|
|
26
|
+
msalSubject$: msalSubject$.asObservable(),
|
|
27
|
+
inProgress$: inProgress$.asObservable()
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
TestBed.configureTestingModule({
|
|
31
|
+
providers: [
|
|
32
|
+
AzureService,
|
|
33
|
+
{ provide: MSAL_GUARD_CONFIG, useValue: msalGuardConfig },
|
|
34
|
+
{ provide: MsalService, useValue: msalServiceSpy },
|
|
35
|
+
{ provide: MsalBroadcastService, useValue: msalBroadcastServiceSpy },
|
|
36
|
+
{ provide: Router, useValue: mockRouter }
|
|
37
|
+
]
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
service = TestBed.inject(AzureService);
|
|
41
|
+
msalService = TestBed.inject(MsalService) as jasmine.SpyObj<MsalService>;
|
|
42
|
+
msalInstanceSpy.getAllAccounts.and.returnValue([{}]);
|
|
43
|
+
msalService.instance = msalInstanceSpy;
|
|
44
|
+
msalGuardConfig.interactionType = InteractionType.Redirect;
|
|
45
|
+
|
|
46
|
+
window.onbeforeunload = () => "Oh no!"; // Prevent page reloads during tests
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should be created', () => {
|
|
50
|
+
expect(service).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should initialize properties in the constructor', () => {
|
|
54
|
+
expect(service.isIframe).toBeFalse();
|
|
55
|
+
expect(service.loginDisplay).toBeFalse();
|
|
56
|
+
expect(service.isFirstTime).toBeTrue();
|
|
57
|
+
expect(service.loggedUser$).toBeInstanceOf(BehaviorSubject);
|
|
58
|
+
expect(service.loggedUser$.value).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('initialize method works', () => {
|
|
62
|
+
msalService.handleRedirectObservable.and.returnValue(of({} as AuthenticationResult));
|
|
63
|
+
service.initialize();
|
|
64
|
+
msalSubject$.next({eventType: EventType.ACCOUNT_ADDED} as EventMessage);
|
|
65
|
+
inProgress$.next(InteractionStatus.None);
|
|
66
|
+
expect(msalService.handleRedirectObservable).toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('initialize - should set window.location.pathname to "/" when all accounts are removed', fakeAsync(() => {
|
|
70
|
+
msalService.handleRedirectObservable.and.returnValue(of({} as AuthenticationResult));
|
|
71
|
+
msalInstanceSpy.getAllAccounts.and.returnValue([]);
|
|
72
|
+
service.initialize();
|
|
73
|
+
msalSubject$.next({ eventType: EventType.ACCOUNT_REMOVED } as EventMessage);
|
|
74
|
+
tick();
|
|
75
|
+
expect(mockRouter.navigate).toHaveBeenCalledWith(['/']);
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
it('refreshLoggedUser - should set loggedUser$ to null if no msalUser and isFirstTime is true', fakeAsync(() => {
|
|
79
|
+
service.isFirstTime = true;
|
|
80
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(null);
|
|
81
|
+
service.refreshLoggedUser();
|
|
82
|
+
expect(service.isFirstTime).toBe(false);
|
|
83
|
+
tick();
|
|
84
|
+
expect(service.loggedUser$.value).toBeNull();
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
it('refreshLoggedUser - should set loggedUser$ to null if no msalUser and isFirstTime is false', fakeAsync(() => {
|
|
88
|
+
service.isFirstTime = false;
|
|
89
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(null);
|
|
90
|
+
service.refreshLoggedUser();
|
|
91
|
+
tick();
|
|
92
|
+
expect(service.loggedUser$.value).toBeNull();
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
it('refreshLoggedUser - should set loggedUser$ with user details if msalUser exists', (done) => {
|
|
96
|
+
const msalUser = { name: 'Test User' };
|
|
97
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(msalUser);
|
|
98
|
+
msalInstanceSpy.acquireTokenSilent.and.returnValue(Promise.resolve({ accessToken: fakeToken }));
|
|
99
|
+
|
|
100
|
+
spyOn(window, 'fetch').and.returnValues(
|
|
101
|
+
Promise.resolve(new Response(new Blob())),
|
|
102
|
+
Promise.resolve(new Response('{"value": [{"displayName": "Group"}]}'))
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
service.loggedUser$.subscribe((user) => {
|
|
106
|
+
if (user) {
|
|
107
|
+
expect(user.fullName).toBe(msalUser.name);
|
|
108
|
+
expect(user.avatarSrc).not.toBeUndefined();
|
|
109
|
+
done();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
service.refreshLoggedUser();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('refreshLoggedUser - should handle error when acquiring token silently', (done) => {
|
|
117
|
+
const msalUser = { name: 'Test User' };
|
|
118
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(msalUser);
|
|
119
|
+
msalInstanceSpy.acquireTokenSilent.and.returnValue(Promise.reject(new Error('Error acquiring token silently')));
|
|
120
|
+
|
|
121
|
+
service.refreshLoggedUser();
|
|
122
|
+
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
expect(service.loggedUser$.value!.fullName).toBe(msalUser.name);
|
|
125
|
+
done();
|
|
126
|
+
}, 0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('refreshLoggedUser - should handle error when fetching profile picture', (done) => {
|
|
130
|
+
const msalUser = { name: 'Test User' };
|
|
131
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(msalUser);
|
|
132
|
+
msalInstanceSpy.acquireTokenSilent.and.returnValue(Promise.resolve({ accessToken: fakeToken }));
|
|
133
|
+
|
|
134
|
+
spyOn(window, 'fetch').and.returnValue(Promise.reject(new Error('Error fetching profile picture')));
|
|
135
|
+
|
|
136
|
+
service.refreshLoggedUser();
|
|
137
|
+
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
expect(service.loggedUser$.value!.fullName).toBe(msalUser.name);
|
|
140
|
+
done();
|
|
141
|
+
}, 0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('refreshLoggedUser - should handle 404 error when fetching profile picture', (done) => {
|
|
145
|
+
const msalUser = { name: 'Test User' };
|
|
146
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(msalUser);
|
|
147
|
+
msalInstanceSpy.acquireTokenSilent.and.returnValue(Promise.resolve({ accessToken: fakeToken }));
|
|
148
|
+
|
|
149
|
+
spyOn(window, 'fetch').and.returnValue(Promise.resolve(new Response(null, { status: 404 })));
|
|
150
|
+
|
|
151
|
+
service.refreshLoggedUser();
|
|
152
|
+
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
expect(service.loggedUser$.value!.fullName).toBe(msalUser.name);
|
|
155
|
+
expect(service.loggedUser$.value!.avatarSrc).toBeUndefined();
|
|
156
|
+
done();
|
|
157
|
+
}, 0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('login - should call loginRedirect with authRequest if msalGuardConfig.authRequest is defined', () => {
|
|
161
|
+
msalGuardConfig.authRequest = { scopes: ['User.Read'] }
|
|
162
|
+
|
|
163
|
+
service.login();
|
|
164
|
+
|
|
165
|
+
expect(msalService.loginRedirect).toHaveBeenCalledWith({
|
|
166
|
+
...{ scopes: ['User.Read'] }
|
|
167
|
+
} as RedirectRequest);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('login - should call loginRedirect without parameters if msalGuardConfig.authRequest is not defined', () => {
|
|
171
|
+
msalGuardConfig.authRequest = undefined;
|
|
172
|
+
|
|
173
|
+
service.login();
|
|
174
|
+
|
|
175
|
+
expect(msalService.loginRedirect).toHaveBeenCalledWith();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('logout - should call logout on msalService', () => {
|
|
179
|
+
service.logout();
|
|
180
|
+
expect(msalService.logout).toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('checkAndSetActiveAccount - should set the first account as active if no active account is set', () => {
|
|
184
|
+
msalInstanceSpy.setActiveAccount.calls.reset();
|
|
185
|
+
msalInstanceSpy.getActiveAccount.calls.reset();
|
|
186
|
+
msalInstanceSpy.getAllAccounts.calls.reset();
|
|
187
|
+
const accounts = [{ username: 'testuser' }];
|
|
188
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(null);
|
|
189
|
+
msalInstanceSpy.getAllAccounts.and.returnValue(accounts);
|
|
190
|
+
service.checkAndSetActiveAccount();
|
|
191
|
+
expect(msalInstanceSpy.setActiveAccount).toHaveBeenCalledWith(accounts[0]);
|
|
192
|
+
expect(msalInstanceSpy.getActiveAccount).toHaveBeenCalled();
|
|
193
|
+
expect(msalInstanceSpy.getAllAccounts).toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('checkAndSetActiveAccount - should not set active account if an active account is already set', () => {
|
|
197
|
+
msalInstanceSpy.setActiveAccount.calls.reset();
|
|
198
|
+
msalInstanceSpy.getActiveAccount.calls.reset();
|
|
199
|
+
const activeAccount = { username: 'activeuser' };
|
|
200
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(activeAccount);
|
|
201
|
+
msalInstanceSpy.acquireTokenSilent.and.returnValue(Promise.resolve({ accessToken: fakeToken }));
|
|
202
|
+
spyOn(window, 'fetch').and.returnValue(Promise.resolve(new Response(new Blob())));
|
|
203
|
+
service.checkAndSetActiveAccount();
|
|
204
|
+
expect(msalInstanceSpy.setActiveAccount).not.toHaveBeenCalled();
|
|
205
|
+
expect(msalInstanceSpy.getActiveAccount).toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('checkAndSetActiveAccount - should not set active account if there are no accounts', () => {
|
|
209
|
+
msalInstanceSpy.setActiveAccount.calls.reset();
|
|
210
|
+
msalInstanceSpy.getActiveAccount.calls.reset();
|
|
211
|
+
msalInstanceSpy.getAllAccounts.calls.reset();
|
|
212
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(null);
|
|
213
|
+
msalInstanceSpy.getAllAccounts.and.returnValue([]);
|
|
214
|
+
service.checkAndSetActiveAccount();
|
|
215
|
+
expect(msalInstanceSpy.setActiveAccount).not.toHaveBeenCalled();
|
|
216
|
+
expect(msalInstanceSpy.getActiveAccount).toHaveBeenCalled();
|
|
217
|
+
expect(msalInstanceSpy.getAllAccounts).toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('checkAndSetActiveAccount - should call refreshLoggedUser', () => {
|
|
221
|
+
msalInstanceSpy.setActiveAccount.calls.reset();
|
|
222
|
+
msalInstanceSpy.getActiveAccount.calls.reset();
|
|
223
|
+
msalInstanceSpy.getAllAccounts.calls.reset();
|
|
224
|
+
spyOn(service, 'refreshLoggedUser');
|
|
225
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(null);
|
|
226
|
+
msalInstanceSpy.getAllAccounts.and.returnValue([{ username: 'testuser' }]);
|
|
227
|
+
service.checkAndSetActiveAccount();
|
|
228
|
+
expect(service.refreshLoggedUser).toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('ngOnDestroy - should complete the _destroying$ subject', () => {
|
|
232
|
+
spyOn(service[destroyingMethodName], 'next');
|
|
233
|
+
spyOn(service[destroyingMethodName], 'complete');
|
|
234
|
+
|
|
235
|
+
service.ngOnDestroy();
|
|
236
|
+
|
|
237
|
+
expect(service[destroyingMethodName].next).toHaveBeenCalledWith(undefined);
|
|
238
|
+
expect(service[destroyingMethodName].complete).toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('getIdToken - should return the idToken of the active account', () => {
|
|
242
|
+
const activeAccount = { idToken: 'test-id-token' };
|
|
243
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(activeAccount);
|
|
244
|
+
|
|
245
|
+
const token = service.getIdToken();
|
|
246
|
+
|
|
247
|
+
expect(token).toBe('test-id-token');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('getIdToken - should return an empty string if no active account', () => {
|
|
251
|
+
msalInstanceSpy.getActiveAccount.and.returnValue(null);
|
|
252
|
+
|
|
253
|
+
const token = service.getIdToken();
|
|
254
|
+
|
|
255
|
+
expect(token).toBe('');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('refreshToken - should call acquireTokenSilent with correct scopes', () => {
|
|
259
|
+
const acquireTokenSilentSpy = msalInstanceSpy.acquireTokenSilent.and.returnValue(Promise.resolve({ accessToken: fakeToken }));
|
|
260
|
+
|
|
261
|
+
service.refreshToken();
|
|
262
|
+
|
|
263
|
+
expect(acquireTokenSilentSpy).toHaveBeenCalledWith({ scopes: ["User.Read"] });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('refreshToken - should return a promise that resolves with the access token', async () => {
|
|
267
|
+
const expectedToken = { accessToken: fakeToken } as AuthenticationResult;
|
|
268
|
+
msalInstanceSpy.acquireTokenSilent.and.returnValue(Promise.resolve(expectedToken));
|
|
269
|
+
|
|
270
|
+
const result = await service.refreshToken();
|
|
271
|
+
|
|
272
|
+
expect(result).toEqual(expectedToken);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('refreshToken - should return a promise that rejects with an error', async () => {
|
|
276
|
+
const expectedError = new Error('Error acquiring token silently');
|
|
277
|
+
msalInstanceSpy.acquireTokenSilent.and.returnValue(Promise.reject(expectedError));
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await service.refreshToken();
|
|
281
|
+
fail('Expected promise to be rejected');
|
|
282
|
+
} catch (error) {
|
|
283
|
+
expect(error).toEqual(expectedError);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('getRefreshedAccessToken - should return an observable that emits the access token', (done) => {
|
|
288
|
+
const expectedToken = 'test-access-token';
|
|
289
|
+
const authResult = { accessToken: expectedToken } as AuthenticationResult;
|
|
290
|
+
msalInstanceSpy.acquireTokenSilent.and.returnValue(Promise.resolve(authResult));
|
|
291
|
+
|
|
292
|
+
service.getRefreshedAccessToken().subscribe((token) => {
|
|
293
|
+
expect(token).toEqual(expectedToken);
|
|
294
|
+
done();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('getRefreshedAccessToken - should handle error when acquiring token fails', (done) => {
|
|
299
|
+
const expectedError = new Error('Error acquiring token');
|
|
300
|
+
msalInstanceSpy.acquireTokenSilent.and.returnValue(Promise.reject(expectedError));
|
|
301
|
+
|
|
302
|
+
service.getRefreshedAccessToken().subscribe({
|
|
303
|
+
next: () => fail('Expected observable to error'),
|
|
304
|
+
error: (error) => {
|
|
305
|
+
expect(error).toEqual(expectedError);
|
|
306
|
+
done();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Inject, Injectable, OnDestroy } from "@angular/core";
|
|
2
|
+
import { MSAL_GUARD_CONFIG, MsalBroadcastService, MsalGuardConfiguration, MsalService } from "@azure/msal-angular";
|
|
3
|
+
import { AuthenticationResult, EventMessage, EventType, InteractionStatus, RedirectRequest } from "@azure/msal-browser";
|
|
4
|
+
import { AppShellUser } from "@appshell/ngx-appshell";
|
|
5
|
+
import { BehaviorSubject, filter, from, map, Observable, Subject, switchMap, takeUntil } from "rxjs";
|
|
6
|
+
import { Router } from "@angular/router";
|
|
7
|
+
|
|
8
|
+
@Injectable({
|
|
9
|
+
providedIn: 'root'
|
|
10
|
+
})
|
|
11
|
+
export class AzureService implements OnDestroy {
|
|
12
|
+
isIframe = false;
|
|
13
|
+
loginDisplay = false;
|
|
14
|
+
private readonly _destroying$ = new Subject<void>();
|
|
15
|
+
|
|
16
|
+
isFirstTime = true;
|
|
17
|
+
loggedUser$ = new BehaviorSubject<AppShellUser | null >(null);
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
@Inject(MSAL_GUARD_CONFIG) private readonly msalGuardConfig: MsalGuardConfiguration,
|
|
21
|
+
private readonly msalService: MsalService,
|
|
22
|
+
private readonly msalBroadcastService: MsalBroadcastService,
|
|
23
|
+
private readonly router: Router
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
initialize() {
|
|
27
|
+
this.msalService.handleRedirectObservable().subscribe();
|
|
28
|
+
this.isIframe = window !== window.parent && !window.opener; // Remove this line to use Angular Universal
|
|
29
|
+
|
|
30
|
+
this.setLoginDisplay();
|
|
31
|
+
|
|
32
|
+
this.msalService.instance.enableAccountStorageEvents(); // Optional - This will enable ACCOUNT_ADDED and ACCOUNT_REMOVED events emitted when a user logs in or out of another tab or window
|
|
33
|
+
this.msalBroadcastService.msalSubject$
|
|
34
|
+
.pipe(
|
|
35
|
+
filter((msg: EventMessage) =>
|
|
36
|
+
msg.eventType === EventType.ACCOUNT_ADDED ||
|
|
37
|
+
msg.eventType === EventType.ACCOUNT_REMOVED
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
.subscribe(() => {
|
|
41
|
+
if (this.msalService.instance.getAllAccounts().length === 0) {
|
|
42
|
+
this.router.navigate(['/']);
|
|
43
|
+
} else {
|
|
44
|
+
this.setLoginDisplay();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.msalBroadcastService.inProgress$
|
|
49
|
+
.pipe(
|
|
50
|
+
filter((status: InteractionStatus) => status === InteractionStatus.None),
|
|
51
|
+
takeUntil(this._destroying$)
|
|
52
|
+
)
|
|
53
|
+
.subscribe(() => {
|
|
54
|
+
this.setLoginDisplay();
|
|
55
|
+
this.checkAndSetActiveAccount();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setLoginDisplay() {
|
|
60
|
+
this.loginDisplay = this.msalService.instance.getAllAccounts().length > 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getIdToken(): string {
|
|
64
|
+
return this.msalService.instance.getActiveAccount()?.idToken ?? '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
refreshToken(): Promise<AuthenticationResult> {
|
|
68
|
+
return this.msalService.instance.acquireTokenSilent({scopes: ["User.Read"]});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getRefreshedAccessToken(): Observable<string> {
|
|
72
|
+
return from(this.refreshToken()).pipe(
|
|
73
|
+
map((azureData: AuthenticationResult) => azureData.accessToken)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
refreshLoggedUser(): void {
|
|
78
|
+
const msalUser = this.msalService.instance.getActiveAccount();
|
|
79
|
+
if (!msalUser) {
|
|
80
|
+
if(this.isFirstTime && !this.isIframe) {
|
|
81
|
+
this.isFirstTime = false;
|
|
82
|
+
// Add a small delay to prevent rapid loops
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
this.login();
|
|
85
|
+
}, 100);
|
|
86
|
+
} else {
|
|
87
|
+
this.loggedUser$.next(null);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
const loggedUser = {
|
|
91
|
+
fullName: msalUser.name,
|
|
92
|
+
username: msalUser.username,
|
|
93
|
+
avatarSrc: undefined
|
|
94
|
+
} as AppShellUser;
|
|
95
|
+
|
|
96
|
+
this.msalService.instance.acquireTokenSilent({
|
|
97
|
+
scopes: ["User.Read"]
|
|
98
|
+
})
|
|
99
|
+
.then(async response => {
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch('https://graph.microsoft.com/v1.0/me/photo/$value', {
|
|
102
|
+
headers: {
|
|
103
|
+
'Authorization': `Bearer ${response.accessToken}`
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
console.warn('Microsoft profile picture not found');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const blob = await res.blob();
|
|
111
|
+
loggedUser.avatarSrc = URL.createObjectURL(blob);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('Error fetching profile picture', error);
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
.catch(error => {
|
|
117
|
+
console.error('Error acquiring token silently', error);
|
|
118
|
+
})
|
|
119
|
+
.finally(() => {
|
|
120
|
+
this.loggedUser$.next(loggedUser);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
checkAndSetActiveAccount() {
|
|
126
|
+
/**
|
|
127
|
+
* If no active account set but there are accounts signed in, sets first account to active account
|
|
128
|
+
* To use active account set here, subscribe to inProgress$ first in your component
|
|
129
|
+
* Note: Basic usage demonstrated. Your app may require more complicated account selection logic
|
|
130
|
+
*/
|
|
131
|
+
const activeAccount = this.msalService.instance.getActiveAccount();
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
!activeAccount &&
|
|
135
|
+
this.msalService.instance.getAllAccounts().length > 0
|
|
136
|
+
) {
|
|
137
|
+
const accounts = this.msalService.instance.getAllAccounts();
|
|
138
|
+
this.msalService.instance.setActiveAccount(accounts[0]);
|
|
139
|
+
}
|
|
140
|
+
this.refreshLoggedUser();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
login() {
|
|
144
|
+
if (this.msalGuardConfig.authRequest) {
|
|
145
|
+
this.msalService.loginRedirect({
|
|
146
|
+
...this.msalGuardConfig.authRequest,
|
|
147
|
+
} as RedirectRequest);
|
|
148
|
+
} else {
|
|
149
|
+
this.msalService.loginRedirect();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
logout() {
|
|
154
|
+
this.msalService.logout();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ngOnDestroy(): void {
|
|
158
|
+
this._destroying$.next(undefined);
|
|
159
|
+
this._destroying$.complete();
|
|
160
|
+
}
|
|
161
|
+
}
|