Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AB#28579 load programs fast #5398

Merged
merged 5 commits into from
Jun 13, 2024
Merged
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
12 changes: 11 additions & 1 deletion interfaces/Portal/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { MsalService } from '@azure/msal-angular';
import { Subscription } from 'rxjs';
import { environment } from 'src/environments/environment';
import { AppRoutes } from './app-routes.enum';
import { AuthService } from './auth/auth.service';
import { LanguageService } from './services/language.service';
import { LoggingService } from './services/logging.service';
Expand Down Expand Up @@ -32,14 +33,23 @@ export class AppComponent implements OnInit, OnDestroy {
}
}

public ngOnInit(): void {
public async ngOnInit() {
aberonni marked this conversation as resolved.
Show resolved Hide resolved
if (this.useSso) {
this.authService.logoutNonSsoUser();

this.msalSubscription = this.msalService
.handleRedirectObservable()
.subscribe();
}

if (
// Do not check the current users' logged-in state on "login-related" pages:
![AppRoutes.login, AppRoutes.auth].includes(
window.location.pathname.substring(1) as AppRoutes,
)
) {
this.authService.refreshCurrentUser();
}
}

public ngOnDestroy(): void {
Expand Down
25 changes: 19 additions & 6 deletions interfaces/Portal/src/app/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ export class AuthService {
return this.getUserFromStorage() !== null;
}

public getAssignedProgramIds(user?: User | null): number[] {
if (!user) {
user = this.getUserFromStorage();
}
const assignedProgramIds = user
? Object.keys(user.permissions).map(Number)
: [];
return assignedProgramIds;
}

private isAssignedToProgram(programId: number, user?: User | null): boolean {
if (!user) {
user = this.getUserFromStorage();
Expand Down Expand Up @@ -142,7 +152,7 @@ export class AuthService {
return {
username: user.username,
permissions: user.permissions,
expires: user.expires ? user.expires : '',
expires: user.expires ? user.expires : undefined,
isAdmin: user.isAdmin,
isEntraUser: user.isEntraUser,
};
Expand Down Expand Up @@ -181,19 +191,22 @@ export class AuthService {
});
}

// TODO: Think of a better name for this method
public async processAzureAuthSuccess(redirectToHome = false): Promise<void> {
public async refreshCurrentUser() {
const userDto = await this.programsService.getCurrentUser();

if (!userDto || !userDto.user) {
localStorage.removeItem(USER_KEY);
this.router.navigate(['/', AppRoutes.login]);
await this.logout();
return;
}

await this.checkSsoTokenExpirationDate();
this.setUserInStorage(userDto.user);
this.updateAuthenticationState();
}

// TODO: Think of a better name for this method
public async processAzureAuthSuccess(redirectToHome = false) {
await this.refreshCurrentUser();
await this.checkSsoTokenExpirationDate();

if (redirectToHome) {
setTimeout(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { AuthService } from 'src/app/auth/auth.service';
import { Program, ProgramStats } from '../../../../models/program.model';
import { ProgramsServiceApiService } from '../../../../services/programs-service-api.service';
import { TranslatableStringService } from '../../../../services/translatable-string.service';
Expand All @@ -15,6 +16,7 @@ export class ProgramsListComponent implements OnInit {

constructor(
private programsService: ProgramsServiceApiService,
private authService: AuthService,
private translatableString: TranslatableStringService,
) {}

Expand All @@ -27,9 +29,13 @@ export class ProgramsListComponent implements OnInit {

private async refresh() {
this.loading = true;
const programs = await this.programsService.getAllPrograms();
this.programStats = await this.programsService.getAllProgramsStats(
programs.map((p) => p.id),
const programIds = this.authService.getAssignedProgramIds();
this.programStats =
await this.programsService.getAllProgramsStats(programIds);
const programs = await Promise.all(
programIds.map((programId) =>
this.programsService.getProgramById(programId),
),
);
this.items = this.translateProperties(programs).sort((a, b) =>
a.created <= b.created ? -1 : 1,
Expand Down
6 changes: 3 additions & 3 deletions interfaces/Portal/src/app/pages/iframe/recipient.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ export class RecipientPage implements OnInit, OnDestroy {
}> {
const programsMap = {};

const programs = await this.progamsServiceApiService.getAllPrograms();
const programIds = this.authService.getAssignedProgramIds();

const detailedPrograms = [];
for (const program of programs) {
for (const id of programIds) {
detailedPrograms.push(
await this.progamsServiceApiService.getProgramById(program.id),
await this.progamsServiceApiService.getProgramById(id),
);
}

Expand Down
15 changes: 0 additions & 15 deletions interfaces/Portal/src/app/services/programs-service-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,6 @@ export class ProgramsServiceApiService {
);
}

getAllPrograms(): Promise<Program[]> {
return this.apiService
.get(environment.url_121_service_api, '/programs/assigned/all')
.then((response) => {
if (response && response.programs) {
return response.programs;
}
return [];
})
.catch((error) => {
console.error('Error: ', error);
return [];
});
}

async getAllProgramsStats(programIds: number[]): Promise<ProgramStats[]> {
const programStats: ProgramStats[] = [];

Expand Down
1 change: 0 additions & 1 deletion services/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ SECRETS_121_SERVICE_SECRET=121_service_secret
# -----------------

# To enable single-sign-on via Azure Entra ID, use: `TRUE` to enable, leave empty or out to disable.
# NOTE: Not used at the moment, is is set in front-end now. Should be moved to back-end though.
USE_SSO_AZURE_ENTRA=

# Azure Entra environment-specific IDs.
Expand Down
7 changes: 0 additions & 7 deletions services/121-service/src/programs/program.interface.ts

This file was deleted.

18 changes: 0 additions & 18 deletions services/121-service/src/programs/programs.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { UpdateProgramDto } from '@121-service/src/programs/dto/update-program.d
import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity';
import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity';
import { ProgramEntity } from '@121-service/src/programs/program.entity';
import { ProgramsRO } from '@121-service/src/programs/program.interface';
import { ProgramService } from '@121-service/src/programs/programs.service';
import { Attribute } from '@121-service/src/registration/enum/custom-data-attributes';
import { SecretDto } from '@121-service/src/scripts/scripts.controller';
Expand Down Expand Up @@ -95,23 +94,6 @@ export class ProgramController {
}
}

@AuthenticatedUser()
@ApiOperation({ summary: 'Get all assigned programs for a user' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Return all assigned programs for a user.',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: 'No user detectable from cookie or no cookie present',
})
// TODO: REFACTOR: into GET /api/users/:userid/programs
@Get('assigned/all')
public async getAssignedPrograms(@Req() req: any): Promise<ProgramsRO> {
const userId = req.user.id;
return await this.programService.getAssignedPrograms(userId);
}

@AuthenticatedUser({ isAdmin: true })
@ApiOperation({
summary: `Create a program.`,
Expand Down
50 changes: 5 additions & 45 deletions services/121-service/src/programs/programs.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { ProgramFspConfigurationEntity } from '@121-service/src/programs/fsp-con
import { ProgramCustomAttributeEntity } from '@121-service/src/programs/program-custom-attribute.entity';
import { ProgramQuestionEntity } from '@121-service/src/programs/program-question.entity';
import { ProgramEntity } from '@121-service/src/programs/program.entity';
import { ProgramsRO } from '@121-service/src/programs/program.interface';
import { overwriteProgramFspDisplayName } from '@121-service/src/programs/utils/overwrite-fsp-display-name.helper';
import { RegistrationDataInfo } from '@121-service/src/registration/dto/registration-data-relation.model';
import { nameConstraintQuestionsArray } from '@121-service/src/shared/const';
Expand All @@ -29,8 +28,7 @@ import { DefaultUserRole } from '@121-service/src/user/user-role.enum';
import { UserService } from '@121-service/src/user/user.service';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { omit } from 'lodash';
import { DataSource, In, QueryFailedError, Repository } from 'typeorm';
import { DataSource, QueryFailedError, Repository } from 'typeorm';

interface FoundProgram
extends Omit<
Expand Down Expand Up @@ -77,7 +75,6 @@ export class ProgramService {
}

const relations = [
'programQuestions',
'financialServiceProviders',
'financialServiceProviders.questions',
'programFspConfiguration',
Expand All @@ -92,11 +89,14 @@ export class ProgramService {
throw new HttpException({ errors }, HttpStatus.NOT_FOUND);
}

// Program attributes are queried separately because the performance is bad when using relations
// Program attributes and questions are queried separately because the performance is bad when using relations
program.programCustomAttributes =
await this.programCustomAttributeRepository.find({
where: { program: { id: programId } },
});
program.programQuestions = await this.programQuestionRepository.find({
where: { program: { id: programId } },
});

program.editableAttributes =
await this.programAttributesService.getPaEditableAttributes(program.id);
Expand Down Expand Up @@ -148,46 +148,6 @@ export class ProgramService {
return programDto;
}

public async getAssignedPrograms(userId: number): Promise<ProgramsRO> {
const user =
await this.userService.findUserProgramAssignmentsOrThrow(userId);
const programIds = user.programAssignments.map((p) => p.program.id);
const programs = await this.programRepository.find({
where: { id: In(programIds) },
relations: [
'programQuestions',
'programCustomAttributes',
'financialServiceProviders',
'financialServiceProviders.questions',
'programFspConfiguration',
],
});
const programsCount = programs.length;

if (programsCount <= 0) {
return {
programs: [],
programsCount,
};
}

return {
programsCount,
programs: programs.map((program) => {
if (program.financialServiceProviders.length > 0) {
program.financialServiceProviders = overwriteProgramFspDisplayName(
program.financialServiceProviders,
program.programFspConfiguration,
);

return omit(program, ['programFspConfiguration']);
}

return program;
}),
};
}

private async validateProgram(programData: CreateProgramDto): Promise<void> {
if (
!programData.financialServiceProviders ||
Expand Down
9 changes: 6 additions & 3 deletions services/121-service/src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,15 @@ export class UserController {
description: 'No user detectable from cookie or no cookie present',
})
public async findMe(@Req() req): Promise<UserRO> {
const username = req.user.username;
if (!username) {
if (!req.user || !req.user.username) {
aberonni marked this conversation as resolved.
Show resolved Hide resolved
const errors = `No user detectable from cookie or no cookie present'`;
throw new HttpException({ errors }, HttpStatus.UNAUTHORIZED);
}
return await this.userService.getUserRoByUsernameOrThrow(username);

return await this.userService.getUserRoByUsernameOrThrow(
req.user.username,
req.user.exp,
);
}

// This endpoint searches users accross all programs, which is needed to add a user to a program
Expand Down
3 changes: 2 additions & 1 deletion services/121-service/src/user/user.interface.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { PermissionEnum } from '@121-service/src/user/enum/permission.enum';

interface UserData {
export interface UserData {
id: number;
username?: string;
permissions: UserPermissions;
isAdmin?: boolean;
isEntraUser?: boolean;
lastLogin?: Date;
expires?: Date;
}

export interface UserRO {
Expand Down
29 changes: 23 additions & 6 deletions services/121-service/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { PermissionEntity } from '@121-service/src/user/permissions.entity';
import { UserRoleEntity } from '@121-service/src/user/user-role.entity';
import { UserType } from '@121-service/src/user/user-type-enum';
import { UserEntity } from '@121-service/src/user/user.entity';
import { UserRO } from '@121-service/src/user/user.interface';
import { UserData, UserRO } from '@121-service/src/user/user.interface';
import { HttpStatus, Inject, Injectable, Scope } from '@nestjs/common';
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { REQUEST } from '@nestjs/core';
Expand Down Expand Up @@ -493,7 +493,10 @@ export class UserService {
return user;
}

public async getUserRoByUsernameOrThrow(username: string): Promise<UserRO> {
public async getUserRoByUsernameOrThrow(
username: string,
tokenExpiration?: number,
): Promise<UserRO> {
const user = await this.userRepository.findOne({
where: { username: username },
relations: [
Expand All @@ -506,7 +509,8 @@ export class UserService {
const errors = `User not found'`;
throw new HttpException({ errors }, HttpStatus.NOT_FOUND);
}
return await this.buildUserRO(user);

return await this.buildUserRO(user, tokenExpiration);
}

public generateJWT(user: UserEntity): string {
Expand Down Expand Up @@ -546,18 +550,31 @@ export class UserService {
}
}

private async buildUserRO(user: UserEntity): Promise<UserRO> {
private async buildUserRO(
user: UserEntity,
tokenExpiration?: number,
): Promise<UserRO> {
const permissions = await this.buildPermissionsObject(user.id);

const userRO = {
const userData: UserData = {
id: user.id,
username: user.username ?? undefined,
permissions,
isAdmin: user.admin,
isEntraUser: user.isEntraUser,
lastLogin: user.lastLogin ?? undefined,
};
return { user: userRO };

// For SSO-users, token expiration is handled by Azure
if (
!process.env.USE_SSO_AZURE_ENTRA &&
!user.isEntraUser &&
tokenExpiration
) {
userData.expires = new Date(tokenExpiration * 1_000);
}

return { user: userData };
}

private async buildPermissionsObject(userId: number): Promise<any> {
Expand Down
Loading