Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 8 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ https://api.reuse.software/badge/github.com/openmfp/portal-ui-lib)](https://api.

This library helps you to set up your angular project consuming [Luigi](https://luigi-project.io/) configuration.

Main features of this library are:
The main features of this library are:

* Enable dynamic Luigi configuration consumption in a microfrontend.
* Provide authentication capabilities with Auth Server
Expand All @@ -28,7 +28,7 @@ Main features of this library are:
- [The luigiExtendedGlobalContextConfigService Option](#the-luigiextendedglobalcontextconfigservice-option)
- [The userSettingsConfigService Option](#the-usersettingsconfigservice-option)
- [The globalSearchConfigService option](#the-globalsearchconfigservice-option)
- [The luigiBreadcrumbConfigService Option](#the-luigibreadcrumbconfigservice-option)
- [The luigiBreadcrumbConfigService Option](#the-headerBarConfigService-option)
- [The userProfileConfigService Option](#the-userprofileconfigservice-option)
- [Functional Services](#functional-services)
- [The luigiAuthEventsCallbacksService Option](#the-luigiautheventscallbacksservice-option)
Expand Down Expand Up @@ -165,7 +165,7 @@ export class StaticSettingsConfigServiceImpl
{
constructor() {}

getInitialStaticSettingsConfig() {
getStaticSettingsConfig() {
const logo = 'assets/my-logo.svg';

return {
Expand All @@ -181,20 +181,9 @@ export class StaticSettingsConfigServiceImpl
// ... the rest of the configuration
};
}

getStaticSettingsConfig() {
return {
...this.getInitialStaticSettingsConfig(),
appLoadingIndicator: {
hideAutomatically: true,
},
// ... the rest of the configuration
}
}
}
```

The `getInitialStaticSettingsConfig()` method is called while constructing the Luigi initial config object.
The `getStaticSettingsConfig()` is called while [Luigi lifecycle hook luigiAfterInit](https://docs.luigi-project.io/docs/lifecycle-hooks?section=luigiafterinit).
The latter adds additional setup to the root of the Luigi configuration object.

Expand Down Expand Up @@ -312,6 +301,7 @@ By default, in the [Luigi's global context](https://docs.luigi-project.io/docs/n
```json
{
"portalContext": {...} ,
"portalBaseUrl": "portal base url",
"userId": "logged in user id",
"userEmail": "logged in user email",
"token": "id token of the logged in user"
Expand Down Expand Up @@ -464,7 +454,7 @@ In your `main.ts` you can provide your custom implementation like so:

```ts
const portalOptions: PortalOptions = {
luigiBreadcrumbConfigService: LuigiBreadcrumbConfigServiceImpl,
headerBarConfigService: HeaderBarConfigServiceImpl,
// ... other portal options
}
```
Expand Down Expand Up @@ -547,7 +537,7 @@ const portalOptions: PortalOptions = {
With this option it is possible to define listeners for [Luigi custom messages](https://docs.luigi-project.io/docs/communication?section=custom-messages).

Custom messages are sent from any part of your application to Luigi and then routed to any other micro frontend application in the same application.
A custom message is sent by using Luigis `sendCustomMessage({ id: 'unique.message.id'});` method (see also the following example).
A custom message is sent by using Luigi `sendCustomMessage({ id: 'unique.message.id'});` method (see also the following example).

```ts
import { inject, Injectable } from '@angular/core';
Expand All @@ -563,7 +553,7 @@ export class MessageService {
}
```

In order to react on such a message in your micro frontend, you have to provide a custom message listener.
To react on such a message in your micro frontend, you have to provide a custom message listener.
You have to specify the corresponding message id you want to listen to.
If there is a match, the callback function `onCustomMessageReceived()` will be called.
Make sure to return a valid Luigi configuration object.
Expand Down Expand Up @@ -617,7 +607,7 @@ export class CustomGlobalNodesServiceImpl implements CustomGlobalNodesService {
private async createGlobalNode1(): LuigiNode {
return {
label: 'Global 1',
entityType: 'global.topnav',
entityType: 'global',
link: '/global_1',
// ... other luigi node properties
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,12 @@ describe('Session Refresh Provider', () => {
provideSessionRefresh(),
{ provide: SessionRefreshService, useValue: sessionRefreshServiceMock },
{ provide: AuthService, useValue: authServiceMock },
{ provide: LuigiCoreService, useValue: luigiCoreServiceMock },
],
});

initializeAutomaticSessionRefresh(
sessionRefreshServiceMock,
authServiceMock,
luigiCoreServiceMock,
);
});

Expand All @@ -52,15 +50,12 @@ describe('Session Refresh Provider', () => {
});

describe('Session refresh initialization', () => {
it('should subscribe and handle AUTH_EXPIRE_SOON event when feature is enabled', fakeAsync(() => {
it('should subscribe and handle AUTH_EXPIRE_SOON event', fakeAsync(() => {
// Act
authEventsSubject.next(AuthEvent.AUTH_EXPIRE_SOON);
tick();

// Assert
expect(luigiCoreServiceMock.isFeatureToggleActive).toHaveBeenCalledWith(
'enableSessionAutoRefresh',
);
expect(sessionRefreshServiceMock.refresh).toHaveBeenCalledTimes(1);
}));

Expand All @@ -73,18 +68,6 @@ describe('Session Refresh Provider', () => {
expect(sessionRefreshServiceMock.refresh).not.toHaveBeenCalled();
}));

it('should not call refresh when feature toggle is disabled', fakeAsync(() => {
// Arrange
luigiCoreServiceMock.isFeatureToggleActive.mockReturnValue(false);

// Act
authEventsSubject.next(AuthEvent.AUTH_EXPIRE_SOON);
tick();

// Assert
expect(sessionRefreshServiceMock.refresh).not.toHaveBeenCalled();
}));

it('should handle refresh failures gracefully', fakeAsync(() => {
try {
// Arrange
Expand Down Expand Up @@ -115,25 +98,6 @@ describe('Session Refresh Provider', () => {
expect(sessionRefreshServiceMock.refresh).toHaveBeenCalledTimes(2);
}));

it('should check feature toggle for each event', fakeAsync(() => {
// Arrange
luigiCoreServiceMock.isFeatureToggleActive
.mockReturnValueOnce(true)
.mockReturnValueOnce(false);

// Act
authEventsSubject.next(AuthEvent.AUTH_EXPIRE_SOON);
tick();
authEventsSubject.next(AuthEvent.AUTH_EXPIRE_SOON);
tick();

// Assert
expect(luigiCoreServiceMock.isFeatureToggleActive).toHaveBeenCalledTimes(
2,
);
expect(sessionRefreshServiceMock.refresh).toHaveBeenCalledTimes(1);
}));

it('should complete subscription when subject is completed', fakeAsync(() => {
// Act
authEventsSubject.complete();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import { AuthEvent } from '../models';
import {
AuthService,
LuigiCoreService,
SessionRefreshService,
} from '../services';
import { AuthService, SessionRefreshService } from '../services';
import { inject, provideAppInitializer } from '@angular/core';
import { filter } from 'rxjs';

export async function initializeAutomaticSessionRefresh(
sessionRefreshService: SessionRefreshService,
authService: AuthService,
luigiCoreService: LuigiCoreService,
) {
authService.authEvents
.pipe(
filter((event: AuthEvent) => event === AuthEvent.AUTH_EXPIRE_SOON),
filter(() =>
luigiCoreService.isFeatureToggleActive('enableSessionAutoRefresh'),
),
)
.pipe(filter((event: AuthEvent) => event === AuthEvent.AUTH_EXPIRE_SOON))
.subscribe({
next: async () => {
try {
Expand All @@ -35,6 +25,5 @@ export const provideSessionRefresh = () =>
initializeAutomaticSessionRefresh(
inject(SessionRefreshService),
inject(AuthService),
inject(LuigiCoreService),
),
);
2 changes: 1 addition & 1 deletion projects/lib/src/lib/models/env.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface AuthTokenData {
expires_in: string;
access_token: string;
access_token?: string;
id_token: string;
}

Expand Down
5 changes: 3 additions & 2 deletions projects/lib/src/lib/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ export interface UserData {
name: string;
email: string;
description: string;
picture: string;
icon: boolean;
icon: string;
initials: string;
userId: string;
}
Expand All @@ -15,5 +14,7 @@ export interface UserTokenData {
family_name: string;
mail: string;
email: string;
userId: string;
icon: string;
sub: string;
}
4 changes: 0 additions & 4 deletions projects/lib/src/lib/portal-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import {
StaticSettingsConfigService,
ThemingService,
UserProfileConfigService,
UserSettingsConfigService,
} from './services';
import { CustomReuseStrategy } from './utilities';
import { provideHttpClient } from '@angular/common/http';
Expand All @@ -64,9 +63,6 @@ export interface PortalOptions {
/** Service providing local configuration services **/
localConfigurationService?: Type<LocalConfigurationService>;

/** Service providing user setting specific configuration **/
userSettingsConfigService?: Type<UserSettingsConfigService>;

/** Service providing global search configuration **/
globalSearchConfigService?: Type<GlobalSearchConfigService>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe('AuthConfigService', () => {
it('should handle userInfoFn correctly', async () => {
const userInfo = {
name: 'Test User',
picture: 'https://example.com/pic.jpg',
icon: 'https://example.com/pic.jpg',
} as UserData;
authServiceMock.getUserInfo.mockReturnValue(userInfo);

Expand All @@ -89,37 +89,6 @@ describe('AuthConfigService', () => {
const result = await userInfoFn?.();

expect(result).toEqual(userInfo);
expect(global.fetch).toHaveBeenCalledWith(
userInfo.picture,
expect.any(Object),
);
});

it('should handle userInfoFn when fetch fails', async () => {
const userPicture = 'https://example.com/pic.jpg';
const userInfo = {
name: 'Test User',
picture: userPicture,
} as UserData;
authServiceMock.getUserInfo.mockReturnValue(userInfo);

envConfigServiceMock.getEnvConfig.mockResolvedValue({
oauthServerUrl: 'https://example.com/oauth',
clientId: 'client-id',
baseDomain: 'https://example.com',
} as any);
const config = await service.getAuthConfig();
const userInfoFn = config?.oAuth2AuthCode?.userInfoFn;

global.fetch = jest.fn().mockRejectedValue(new Error('Fetch failed'));

const result = await userInfoFn?.();

expect(result).toEqual({ ...userInfo, picture: '' });
expect(global.fetch).toHaveBeenCalledWith(
userPicture,
expect.any(Object),
);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,8 @@ export class AuthConfigService {
expirationCheckInterval: 5,
userInfoFn: () => {
const userInfo = this.authService.getUserInfo();

return new Promise((resolve) => {
fetch(userInfo.picture, {
method: 'HEAD',
mode: 'no-cors',
})
.then(() => {
resolve(userInfo);
})
.catch(() => {
userInfo.picture = '';
resolve(userInfo);
});
resolve(userInfo);
});
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,7 @@ export class UserSettingsConfigService {
luigiUserSettings.forEach((userConfig) => {
if (userConfig?.groups) {
Object.keys(userConfig.groups).forEach((groupId) => {
const groupConfig = userConfig.groups[groupId];
settingsGroups[groupId] = groupConfig;
settingsGroups[groupId] = userConfig.groups[groupId];
});
}
});
Expand Down Expand Up @@ -262,7 +261,7 @@ export class UserSettingsConfigService {
settings.frame_userAccount = {
label: 'USERSETTINGSDIALOG_USER_ACCOUNT',
sublabel: userInfo.name,
icon: imageUrl || 'person-placeholder',
icon: imageUrl || userInfo.icon || 'person-placeholder',
title: userInfo.name,
initials: userInfo.initials,
iconClassAttribute:
Expand Down
6 changes: 0 additions & 6 deletions projects/lib/src/lib/services/portal/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,8 @@ describe('AuthService', () => {
expect(service.getUserInfo()).toEqual({
description: '',
email: '',
icon: false,
initials: '',
name: ' ',
picture: '',
userId: '',
});
});
Expand Down Expand Up @@ -188,9 +186,7 @@ describe('AuthService', () => {
name: 'John Doe',
email: '[email protected]',
description: '[email protected]',
picture: '',
userId: 'user123',
icon: false,
initials: 'JD',
});
});
Expand All @@ -207,8 +203,6 @@ describe('AuthService', () => {
name: ' ',
email: '[email protected]',
description: '[email protected]',
picture: '',
icon: false,
initials: '',
userId: 'user123',
});
Expand Down
5 changes: 2 additions & 3 deletions projects/lib/src/lib/services/portal/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,9 @@ export class AuthService {
name: `${firstName} ${lastName}`,
email: user.mail || user.email || '',
description: user.mail || user.email || '',
picture: '',
icon: false,
icon: user.icon,
initials: initialsFirstName + initialsLastName,
userId: user.sub || '',
userId: user.userId || user.sub || '',
};
}

Expand Down