diff --git a/apps/services/auth/admin-api/project.json b/apps/services/auth/admin-api/project.json index 2866faa0019a..e5d91fd92127 100644 --- a/apps/services/auth/admin-api/project.json +++ b/apps/services/auth/admin-api/project.json @@ -57,6 +57,17 @@ }, "docker-express": { "executor": "Intentionally left blank, only so this target is valid when using `nx show projects --with-target docker-express`" + }, + "dev": { + "executor": "nx:run-commands", + "options": { + "commands": [ + { + "command": "yarn start --project services-auth-admin-api" + } + ], + "parallel": true + } } } } diff --git a/apps/services/auth/delegation-api/project.json b/apps/services/auth/delegation-api/project.json index 366532998b91..66890516fc3c 100644 --- a/apps/services/auth/delegation-api/project.json +++ b/apps/services/auth/delegation-api/project.json @@ -28,6 +28,17 @@ "buildTarget": "services-auth-delegation-api:build" } }, + "dev": { + "executor": "nx:run-commands", + "options": { + "commands": [ + { + "command": "yarn start services-auth-delegation-api" + } + ], + "parallel": true + } + }, "lint": { "executor": "@nx/eslint:lint" }, diff --git a/apps/services/auth/ids-api/project.json b/apps/services/auth/ids-api/project.json index cfefdd77acda..b64e70508680 100644 --- a/apps/services/auth/ids-api/project.json +++ b/apps/services/auth/ids-api/project.json @@ -175,6 +175,17 @@ ], "parallel": true } + }, + "dev": { + "executor": "nx:run-commands", + "options": { + "commands": [ + { + "command": "yarn start services-auth-ids-api" + } + ], + "parallel": true + } } } } diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts index f2dee16513d6..faee06788837 100644 --- a/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts +++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts @@ -1,7 +1,7 @@ import { getModelToken } from '@nestjs/sequelize' -import addDays from 'date-fns/addDays' import request from 'supertest' import { uuid } from 'uuidv4' +import addDays from 'date-fns/addDays' import { ApiScope, @@ -27,12 +27,16 @@ import { } from '@island.is/shared/types' import { createCurrentUser, + createNationalId, createNationalRegistryUser, } from '@island.is/testing/fixtures' import { TestApp } from '@island.is/testing/nest' import { defaultScopes, setupWithAuth } from '../../../../test/setup' -import { getFakeNationalId } from '../../../../test/stubs/genericStubs' +import { + getFakeCompanyNationalId, + getFakeNationalId, +} from '../../../../test/stubs/genericStubs' describe('DelegationsController', () => { describe.each([false, true])( @@ -57,6 +61,8 @@ describe('DelegationsController', () => { clientId: '@island.is/webapp', }) + const representeeNationalId = createNationalId('person') + const scopeValid1 = 'scope/valid1' const scopeValid2 = 'scope/valid2' const scopeValid1and2 = 'scope/valid1and2' @@ -76,7 +82,7 @@ describe('DelegationsController', () => { scopeName: s, })) - const userNationalId = getFakeNationalId() + const userNationalId = createNationalId('person') const user = createCurrentUser({ nationalId: userNationalId, @@ -85,6 +91,8 @@ describe('DelegationsController', () => { }) const domain = createDomain() + let nationalRegistryApiSpy: jest.SpyInstance + let nationalRegistryV3ApiSpy: jest.SpyInstance beforeAll(async () => { app = await setupWithAuth({ @@ -117,36 +125,214 @@ describe('DelegationsController', () => { >(getModelToken(DelegationDelegationType)) nationalRegistryApi = app.get(NationalRegistryClientService) nationalRegistryV3Api = app.get(NationalRegistryV3ClientService) + factory = new FixtureFactory(app) + + client.supportedDelegationTypes = [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + ] + await factory.createClient(client) + + nationalRegistryApiSpy = jest + .spyOn(nationalRegistryApi, 'getIndividual') + .mockImplementation(async (id) => { + const user = createNationalRegistryUser({ + nationalId: representeeNationalId, + }) + + return user ?? null + }) + + nationalRegistryV3ApiSpy = jest + .spyOn(nationalRegistryV3Api, 'getAllDataIndividual') + .mockImplementation(async () => { + const user = createNationalRegistryUser({ + nationalId: representeeNationalId, + }) + + return { kennitala: user.nationalId, nafn: user.name } + }) + const nationalRegistryV3FeatureService = app.get( NationalRegistryV3FeatureService, ) jest .spyOn(nationalRegistryV3FeatureService, 'getValue') .mockImplementation(async () => featureFlag) - factory = new FixtureFactory(app) }) afterAll(async () => { await app.cleanUp() + nationalRegistryV3ApiSpy.mockClear() + nationalRegistryApiSpy.mockClear() }) - describe('GET with general mandate delegation type', () => { - const representeeNationalId = getFakeNationalId() - let nationalRegistryApiSpy: jest.SpyInstance - let nationalRegistryV3ApiSpy: jest.SpyInstance + describe('GET with general mandate delegation type for company', () => { + const companyNationalId = getFakeCompanyNationalId() + const scopeNames = [ 'api-scope/generalMandate1', 'api-scope/generalMandate2', 'api-scope/generalMandate3', + 'api-scope/procuration1', + 'api-scope/procuration2', ] beforeAll(async () => { - client.supportedDelegationTypes = [ - AuthDelegationType.GeneralMandate, - AuthDelegationType.LegalGuardian, + const delegations = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Company', + fromNationalId: companyNationalId, + toNationalId: userNationalId, + toName: 'Person', + }) + + await delegationDelegationTypeModel.create({ + delegationId: delegations.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) + + await apiScopeModel.bulkCreate( + scopeNames.map((name) => ({ + name, + domainName: domain.name, + enabled: true, + description: `${name}: description`, + displayName: `${name}: display name`, + })), + ) + + await apiScopeDelegationTypeModel.bulkCreate([ + { + apiScopeName: scopeNames[0], + delegationType: AuthDelegationType.GeneralMandate, + }, + { + apiScopeName: scopeNames[1], + delegationType: AuthDelegationType.GeneralMandate, + }, + { + apiScopeName: scopeNames[3], + delegationType: AuthDelegationType.ProcurationHolder, + }, + { + apiScopeName: scopeNames[4], + delegationType: AuthDelegationType.ProcurationHolder, + }, + ]) + }) + + afterAll(async () => { + await apiScopeDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await apiScopeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + }) + + it('should return mergedDelegationDTO with the generalMandate', async () => { + const response = await server.get('/v2/delegations') + + expect(response.status).toEqual(200) + expect(response.body).toHaveLength(1) + }) + + it('should return all general mandate scopes and other preset scopes', async () => { + const response = await server.get('/delegations/scopes').query({ + fromNationalId: companyNationalId, + delegationType: [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.ProcurationHolder, + ], + }) + + const expected = [ + scopeNames[0], + scopeNames[1], + scopeNames[3], + scopeNames[4], + ] + + expect(response.status).toEqual(200) + expect(response.body).toEqual(expect.arrayContaining(expected)) + expect(response.body).toHaveLength(expected.length) + }) + + it('should return all general mandate scopes and all procuration scopes', async () => { + const response = await server.get('/delegations/scopes').query({ + fromNationalId: companyNationalId, + delegationType: [AuthDelegationType.GeneralMandate], + }) + + const expected = [ + scopeNames[0], + scopeNames[1], + scopeNames[3], + scopeNames[4], ] - await factory.createClient(client) + expect(response.status).toEqual(200) + expect(response.body).toEqual(expect.arrayContaining(expected)) + expect(response.body).toHaveLength(expected.length) + }) + + it('should return all general mandate scopes, and not procuration scopes since from nationalId is person', async () => { + // Assert + const delegation = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'FromPersonPerson', + fromNationalId: representeeNationalId, + toNationalId: userNationalId, + toName: 'Person', + }) + + await delegationDelegationTypeModel.create({ + delegationId: delegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) + + // Act + const response = await server.get('/delegations/scopes').query({ + fromNationalId: representeeNationalId, + delegationType: [AuthDelegationType.GeneralMandate], + }) + + const expected = [scopeNames[0], scopeNames[1]] + + expect(response.status).toEqual(200) + expect(response.body).toEqual(expect.arrayContaining(expected)) + expect(response.body).toHaveLength(expected.length) + }) + }) + + describe('GET with general mandate delegation type', () => { + const scopeNames = [ + 'api-scope/generalMandate1', + 'api-scope/generalMandate2', + 'api-scope/generalMandate3', + ] + + beforeAll(async () => { const delegations = await delegationModel.create({ id: uuid(), fromDisplayName: 'Test', @@ -187,32 +373,39 @@ describe('DelegationsController', () => { delegationType: AuthDelegationType.GeneralMandate, }, ]) - - nationalRegistryApiSpy = jest - .spyOn(nationalRegistryApi, 'getIndividual') - .mockImplementation(async (id) => { - const user = createNationalRegistryUser({ - nationalId: representeeNationalId, - }) - - return user ?? null - }) - - nationalRegistryV3ApiSpy = jest - .spyOn(nationalRegistryV3Api, 'getAllDataIndividual') - .mockImplementation(async () => { - const user = createNationalRegistryUser({ - nationalId: representeeNationalId, - }) - - return { kennitala: user.nationalId, nafn: user.name } - }) }) afterAll(async () => { - await app.cleanUp() - nationalRegistryApiSpy.mockClear() - nationalRegistryV3ApiSpy.mockClear() + await apiScopeDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await apiScopeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationProviderModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationDelegationTypeModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) + await delegationModel.destroy({ + where: {}, + cascade: true, + truncate: true, + force: true, + }) }) it('should return mergedDelegationDTO with the generalMandate', async () => { diff --git a/apps/services/auth/ids-api/test/stubs/genericStubs.ts b/apps/services/auth/ids-api/test/stubs/genericStubs.ts index 91956b00fcc5..177ad31ae023 100644 --- a/apps/services/auth/ids-api/test/stubs/genericStubs.ts +++ b/apps/services/auth/ids-api/test/stubs/genericStubs.ts @@ -6,6 +6,8 @@ export type NameIdTuple = [name: string, id: string] export const getFakeNationalId = () => faker.helpers.replaceSymbolWithNumber('##########') +export const getFakeCompanyNationalId = () => createNationalId('company') + export const getFakeName = () => faker.fake('{{name.firstName}} {{name.lastName}}') @@ -18,4 +20,5 @@ export default { getFakeNationalId, getFakeName, getFakePerson, + getFakeCompanyNationalId, } diff --git a/apps/services/user-notification/src/app/modules/notifications/me-notifications.controller.ts b/apps/services/user-notification/src/app/modules/notifications/me-notifications.controller.ts index da5d5c7e63a9..26deb3a189a6 100644 --- a/apps/services/user-notification/src/app/modules/notifications/me-notifications.controller.ts +++ b/apps/services/user-notification/src/app/modules/notifications/me-notifications.controller.ts @@ -52,7 +52,7 @@ export class MeNotificationsController { @CurrentUser() user: User, @Query() query: ExtendedPaginationDto, ): Promise { - return this.notificationService.findMany(user, query) + return this.notificationService.findManyWithTemplate(user.nationalId, query) } @Get('/unread-count') diff --git a/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts b/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts index a1425841a8b8..c8f578ce6397 100644 --- a/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts +++ b/apps/services/user-notification/src/app/modules/notifications/notifications.controller.ts @@ -3,10 +3,13 @@ import { Body, Controller, Get, + Headers, + HttpStatus, Inject, Param, Post, Query, + UseGuards, Version, } from '@nestjs/common' import { ApiTags } from '@nestjs/swagger' @@ -19,6 +22,12 @@ import { CreateHnippNotificationDto } from './dto/createHnippNotification.dto' import { HnippTemplate } from './dto/hnippTemplate.response' import { NotificationsService } from './notifications.service' import type { Locale } from '@island.is/shared/types' +import { + ExtendedPaginationDto, + PaginatedNotificationDto, +} from './dto/notification.dto' +import { IdsUserGuard, Scopes, ScopesGuard } from '@island.is/auth-nest-tools' +import { AdminPortalScope } from '@island.is/auth/scopes' @Controller('notifications') @ApiTags('notifications') @@ -85,6 +94,20 @@ export class NotificationsController { return await this.notificationsService.getTemplate(templateId, locale) } + @UseGuards(IdsUserGuard, ScopesGuard) + @Scopes(AdminPortalScope.serviceDesk) + @Get('/') + @Documentation({ + summary: 'Returns a paginated list of notifications for a national id', + response: { status: HttpStatus.OK, type: PaginatedNotificationDto }, + }) + findMany( + @Headers('X-Query-National-Id') nationalId: string, + @Query() query: ExtendedPaginationDto, + ): Promise { + return this.notificationsService.findMany(nationalId, query) + } + @Documentation({ summary: 'Creates a new notification and adds to queue', includeNoContentResponse: true, diff --git a/apps/services/user-notification/src/app/modules/notifications/notifications.service.ts b/apps/services/user-notification/src/app/modules/notifications/notifications.service.ts index 8d8e937ff0f7..4116120d2744 100644 --- a/apps/services/user-notification/src/app/modules/notifications/notifications.service.ts +++ b/apps/services/user-notification/src/app/modules/notifications/notifications.service.ts @@ -260,8 +260,24 @@ export class NotificationsService { ) } - async findMany( - user: User, + findMany( + nationalId: string, + query: ExtendedPaginationDto, + ): Promise { + return paginate({ + Model: this.notificationModel, + limit: query.limit || 10, + after: query.after || '', + before: query.before, + primaryKeyField: 'id', + orderOption: [['id', 'DESC']], + where: { recipient: nationalId }, + attributes: ['id', 'messageId', 'senderId', 'created', 'updated'], + }) + } + + async findManyWithTemplate( + nationalId: string, query: ExtendedPaginationDto, ): Promise { const locale = mapToLocale(query.locale as Locale) @@ -273,7 +289,7 @@ export class NotificationsService { before: query.before, primaryKeyField: 'id', orderOption: [['id', 'DESC']], - where: { recipient: user.nationalId }, + where: { recipient: nationalId }, }) const formattedNotifications = await Promise.all( diff --git a/apps/services/user-notification/src/app/modules/notifications/tests/notifications.service.spec.ts b/apps/services/user-notification/src/app/modules/notifications/tests/notifications.service.spec.ts index 8820d8ddd2cb..8456f9a5333e 100644 --- a/apps/services/user-notification/src/app/modules/notifications/tests/notifications.service.spec.ts +++ b/apps/services/user-notification/src/app/modules/notifications/tests/notifications.service.spec.ts @@ -159,7 +159,9 @@ describe('NotificationsService', () => { .spyOn(service, 'findMany') .mockImplementation(async () => mockedResponse) - expect(await service.findMany(user, query)).toBe(mockedResponse) + expect(await service.findMany(user.nationalId, query)).toBe( + mockedResponse, + ) }) }) diff --git a/libs/api/domains/notifications/src/lib/notifications.model.ts b/libs/api/domains/notifications/src/lib/notifications.model.ts index 64436d89583a..2c83f4a8107c 100644 --- a/libs/api/domains/notifications/src/lib/notifications.model.ts +++ b/libs/api/domains/notifications/src/lib/notifications.model.ts @@ -89,6 +89,21 @@ export class Notification { message!: NotificationMessage } +@ObjectType() +export class AdminNotification { + @Field(() => Int) + id!: number + + @Field(() => ID) + notificationId!: string + + @Field(() => NotificationSender) + sender!: NotificationSender + + @Field(() => GraphQLISODateTime) + sent!: Date +} + @InputType() export class NotificationsInput extends PaginationInput() {} @@ -101,6 +116,11 @@ export class NotificationsResponse extends PaginatedResponse(Notification) { unseenCount?: number } +@ObjectType('AdminNotifications') +export class AdminNotificationsResponse extends PaginatedResponse( + AdminNotification, +) {} + @ObjectType() export class NotificationResponse { @Field(() => Notification) diff --git a/libs/api/domains/notifications/src/lib/notifications.module.ts b/libs/api/domains/notifications/src/lib/notifications.module.ts index 745702eb28bb..231c0d96c879 100644 --- a/libs/api/domains/notifications/src/lib/notifications.module.ts +++ b/libs/api/domains/notifications/src/lib/notifications.module.ts @@ -8,6 +8,8 @@ import { NotificationSenderResolver, } from './notificationsList.resolver' import { NotificationsService } from './notifications.service' +import { NotificationsAdminResolver } from './notificationsAdmin.resolver' +import { NotificationsAdminService } from './notificationsAdmin.service' @Module({ imports: [UserNotificationClientModule], @@ -15,7 +17,9 @@ import { NotificationsService } from './notifications.service' NotificationsResolver, NotificationsListResolver, NotificationSenderResolver, + NotificationsAdminResolver, NotificationsService, + NotificationsAdminService, ], exports: [], }) diff --git a/libs/api/domains/notifications/src/lib/notificationsAdmin.resolver.ts b/libs/api/domains/notifications/src/lib/notificationsAdmin.resolver.ts new file mode 100644 index 000000000000..9afd668e3173 --- /dev/null +++ b/libs/api/domains/notifications/src/lib/notificationsAdmin.resolver.ts @@ -0,0 +1,60 @@ +import { Args, Query, Resolver } from '@nestjs/graphql' +import { IdsUserGuard, CurrentUser } from '@island.is/auth-nest-tools' +import type { User } from '@island.is/auth-nest-tools' +import { Audit } from '@island.is/nest/audit' +import { Inject, UseGuards } from '@nestjs/common' + +import { NotificationsAdminService } from './notificationsAdmin.service' +import { + NotificationsInput, + AdminNotificationsResponse, +} from './notifications.model' +import type { Locale } from '@island.is/shared/types' +import { LOGGER_PROVIDER, type Logger } from '@island.is/logging' + +const LOG_CATEGORY = 'notification-admin-resolver' +export const AUDIT_NAMESPACE = 'notifications-admin-resolver' + +@UseGuards(IdsUserGuard) +@Resolver(() => AdminNotificationsResponse) +@Audit({ namespace: AUDIT_NAMESPACE }) +export class NotificationsAdminResolver { + constructor( + private readonly service: NotificationsAdminService, + @Inject(LOGGER_PROVIDER) private readonly logger: Logger, + ) {} + + @Query(() => AdminNotificationsResponse, { + name: 'adminNotifications', + nullable: true, + }) + @Audit() + async getNotifications( + @Args('nationalId') nationalId: string, + @Args('input', { type: () => NotificationsInput, nullable: true }) + input: NotificationsInput, + @CurrentUser() user: User, + @Args('locale', { type: () => String, nullable: true }) + locale: Locale = 'is', + ): Promise { + let notifications: AdminNotificationsResponse | null + + try { + notifications = await this.service.getNotifications( + locale, + nationalId, + user, + input, + ) + } catch (e) { + this.logger.error('failed to get admin notifications', { + locale, + category: LOG_CATEGORY, + error: e, + }) + throw e + } + + return notifications + } +} diff --git a/libs/api/domains/notifications/src/lib/notificationsAdmin.service.ts b/libs/api/domains/notifications/src/lib/notificationsAdmin.service.ts new file mode 100644 index 000000000000..76089eff1156 --- /dev/null +++ b/libs/api/domains/notifications/src/lib/notificationsAdmin.service.ts @@ -0,0 +1,52 @@ +import { Auth, AuthMiddleware, User } from '@island.is/auth-nest-tools' +import { LOGGER_PROVIDER } from '@island.is/logging' +import type { Logger } from '@island.is/logging' +import { Inject, Injectable } from '@nestjs/common' +import { NotificationsApi } from '@island.is/clients/user-notification' +import type { Locale } from '@island.is/shared/types' +import { + AdminNotificationsResponse, + NotificationsInput, +} from './notifications.model' +import { adminNotificationMapper } from '../utils/helpers' + +@Injectable() +export class NotificationsAdminService { + constructor( + @Inject(LOGGER_PROVIDER) + private logger: Logger, + private notificationsApi: NotificationsApi, + ) {} + + notificationsWAuth(auth: Auth) { + return this.notificationsApi.withMiddleware(new AuthMiddleware(auth)) + } + + async getNotifications( + locale: Locale, + nationalId: string, + user: User, + input?: NotificationsInput, + ): Promise { + const notifications = await this.notificationsWAuth( + user, + ).notificationsControllerFindMany({ + xQueryNationalId: nationalId, + locale, + limit: input?.limit, + before: input?.before, + after: input?.after, + }) + + if (!notifications.data) { + this.logger.debug('no admin notification found') + return null + } + + return { + data: notifications.data.map((item) => adminNotificationMapper(item)), + totalCount: notifications.totalCount, + pageInfo: notifications.pageInfo, + } + } +} diff --git a/libs/api/domains/notifications/src/utils/helpers.ts b/libs/api/domains/notifications/src/utils/helpers.ts index 3baac1e1b7fc..87932276f1e9 100644 --- a/libs/api/domains/notifications/src/utils/helpers.ts +++ b/libs/api/domains/notifications/src/utils/helpers.ts @@ -1,5 +1,5 @@ import { RenderedNotificationDto } from '@island.is/clients/user-notification' -import { Notification } from '../lib/notifications.model' +import { AdminNotification, Notification } from '../lib/notifications.model' const cleanString = (str?: string) => { if (!str) { @@ -40,3 +40,13 @@ export const notificationMapper = ( }, }, }) +export const adminNotificationMapper = ( + notification: RenderedNotificationDto, +): AdminNotification => ({ + id: notification.id, + notificationId: notification.messageId, + sent: notification.created, + sender: { + id: notification.senderId, + }, +}) diff --git a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts index 5cbb11d913b5..e1de9e93df1a 100644 --- a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts @@ -12,6 +12,7 @@ import { TicketStatus, ZendeskService, } from '@island.is/clients/zendesk' +import { CompanyRegistryClientService } from '@island.is/clients/rsk/company-registry' import { Delegation } from '../models/delegation.model' import { DelegationAdminCustomDto } from '../dto/delegation-admin-custom.dto' @@ -45,6 +46,7 @@ export class DelegationAdminCustomService { private delegationScopeService: DelegationScopeService, private namesService: NamesService, private sequelize: Sequelize, + private rskCompanyInfoService: CompanyRegistryClientService, ) {} private getZendeskCustomFields(ticket: Ticket): { @@ -276,9 +278,7 @@ export class DelegationAdminCustomService { }) } - if ( - !(kennitala.isPerson(fromNationalId) && kennitala.isPerson(toNationalId)) - ) { + if (!kennitala.isPerson(toNationalId)) { throw new BadRequestException({ message: 'National Ids are not valid', error: ErrorCodes.INPUT_VALIDATION_INVALID_PERSON, @@ -300,7 +300,11 @@ export class DelegationAdminCustomService { }, ): Promise { const [fromDisplayName, toName] = await Promise.all([ - this.namesService.getPersonName(delegation.fromNationalId), + kennitala.isPerson(delegation.fromNationalId) + ? this.namesService.getPersonName(delegation.fromNationalId) + : this.rskCompanyInfoService + .getCompany(delegation.fromNationalId) + .then((company) => company?.name ?? ''), this.namesService.getPersonName(delegation.toNationalId), ]) @@ -310,6 +314,7 @@ export class DelegationAdminCustomService { { fromNationalId: delegation.fromNationalId, toNationalId: delegation.toNationalId, + domainName: null, }, { referenceId: delegation.referenceId, diff --git a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts index d12b1d8af8e4..bb74ed0a4d6e 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts @@ -5,6 +5,7 @@ import addDays from 'date-fns/addDays' import startOfDay from 'date-fns/startOfDay' import { Op, Transaction } from 'sequelize' import { uuid } from 'uuidv4' +import * as kennitala from 'kennitala' import { SyslumennService } from '@island.is/clients/syslumenn' import { logger } from '@island.is/logging' @@ -209,7 +210,14 @@ export class DelegationScopeService { { model: ApiScopeDelegationType, where: { - delegationType: AuthDelegationType.GeneralMandate, + delegationType: { + [Op.or]: kennitala.isCompany(fromNationalId) + ? [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.ProcurationHolder, + ] + : [AuthDelegationType.GeneralMandate], + }, }, }, ], diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts index ee60fb61c692..e11e509095bd 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts @@ -196,17 +196,48 @@ export class DelegationsIncomingCustomService { ) } + /** + * Finds all companies that have a general mandate for the user. + * @param user + * @param clientAllowedApiScopes + * @param requireApiScopes + */ + async findCompanyGeneralMandate( + user: User, + clientAllowedApiScopes: ApiScopeInfo[], + requireApiScopes: boolean, + ): Promise { + const delegations = await this.findAllAvailableGeneralMandate( + user, + clientAllowedApiScopes, + requireApiScopes, + [AuthDelegationType.ProcurationHolder], + ) + + return delegations.filter((d) => kennitala.isCompany(d.fromNationalId)) + } + + /** + * Finds all individuals that have a general mandate for the user. + * @param user + * @param clientAllowedApiScopes + * @param requireApiScopes + * @param supportedDelegationTypes + */ async findAllAvailableGeneralMandate( user: User, clientAllowedApiScopes: ApiScopeInfo[], requireApiScopes: boolean, + supportedDelegationTypes = [AuthDelegationType.GeneralMandate], ): Promise { const customApiScopes = clientAllowedApiScopes.filter( (s) => !s.isAccessControlled && this.filterByCustomScopeRule(s) && - s.supportedDelegationTypes?.some( - (dt) => dt.delegationType === AuthDelegationType.GeneralMandate, + s.supportedDelegationTypes?.some((dt) => + supportedDelegationTypes.includes( + dt.delegationType as AuthDelegationType, + ), ), ) diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts index b2e0775d3b48..05ddc050ee0f 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts @@ -176,6 +176,25 @@ export class DelegationsIncomingService { ) } + // If procuration holder is enabled, we need to get the general mandate delegations + if (types?.includes(AuthDelegationType.ProcurationHolder)) { + const isGeneralMandateDelegationEnabled = + await this.featureFlagService.getValue( + Features.isGeneralMandateDelegationEnabled, + false, + user, + ) + if (isGeneralMandateDelegationEnabled) { + delegationPromises.push( + this.delegationsIncomingCustomService.findCompanyGeneralMandate( + user, + clientAllowedApiScopes, + client.requireApiScopes, + ), + ) + } + } + if (providers.includes(AuthDelegationProvider.CompanyRegistry)) { delegationPromises.push( this.incomingDelegationsCompanyService @@ -293,6 +312,11 @@ export class DelegationsIncomingService { new Map(), ) + // Remove duplicate delegationTypes.. + mergedDelegationMap.forEach((delegation) => { + delegation.types = Array.from(new Set(delegation.types)) + }) + return [...mergedDelegationMap.values()] } diff --git a/libs/nest/pagination/src/lib/paginate.ts b/libs/nest/pagination/src/lib/paginate.ts index e289af92c053..87410488a3f0 100644 --- a/libs/nest/pagination/src/lib/paginate.ts +++ b/libs/nest/pagination/src/lib/paginate.ts @@ -117,6 +117,7 @@ export interface PaginateInput { primaryKeyField: string orderOption: any where?: any + attributes?: any after: string before?: string limit: number @@ -138,6 +139,7 @@ export async function paginate({ after, before, limit, + attributes, ...queryArgs }: PaginateInput): Promise<{ totalCount: number @@ -164,6 +166,7 @@ export async function paginate({ where: paginationWhere, limit, order, + attributes, ...queryArgs, } diff --git a/libs/portals/admin/service-desk/src/lib/messages.ts b/libs/portals/admin/service-desk/src/lib/messages.ts index 8560dcf08ef2..9e9c4008fcfd 100644 --- a/libs/portals/admin/service-desk/src/lib/messages.ts +++ b/libs/portals/admin/service-desk/src/lib/messages.ts @@ -65,6 +65,14 @@ export const m = defineMessages({ id: 'admin-portal.service-desk:info', defaultMessage: 'Upplýsingar: ', }, + notifications: { + id: 'admin-portal.service-desk:notifications', + defaultMessage: 'Tilkynningar: ', + }, + loadMore: { + id: 'admin-portal.service-desk:load-more', + defaultMessage: 'Sjá meira', + }, email: { id: 'admin-portal.service-desk:email', defaultMessage: 'Netfang', diff --git a/libs/portals/admin/service-desk/src/screens/User/User.graphql b/libs/portals/admin/service-desk/src/screens/User/User.graphql index 1d65b549f8b6..dd1f8410069b 100644 --- a/libs/portals/admin/service-desk/src/screens/User/User.graphql +++ b/libs/portals/admin/service-desk/src/screens/User/User.graphql @@ -32,3 +32,24 @@ mutation UpdateUserProfile( locale } } + +query GetAdminNotifications($nationalId: String!, $input: NotificationsInput!) { + adminNotifications(nationalId: $nationalId, input: $input) { + data { + id + notificationId + sender { + id + logoUrl + } + sent + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } +} diff --git a/libs/portals/admin/service-desk/src/screens/User/User.tsx b/libs/portals/admin/service-desk/src/screens/User/User.tsx index 7b16758ac36a..3a132e6512df 100644 --- a/libs/portals/admin/service-desk/src/screens/User/User.tsx +++ b/libs/portals/admin/service-desk/src/screens/User/User.tsx @@ -1,18 +1,34 @@ import format from 'date-fns/format' import { useLoaderData, useNavigate, useRevalidator } from 'react-router-dom' -import { ActionCard, Box, Stack, Text } from '@island.is/island-ui/core' +import { + ActionCard, + Box, + Stack, + Text, + Table as T, + LoadingDots, + SkeletonLoader, +} from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' import { BackButton } from '@island.is/portals/admin/core' import { IntroHeader, formatNationalId } from '@island.is/portals/core' import { dateFormat } from '@island.is/shared/constants' +import InfiniteScroll from 'react-infinite-scroller' -import { ServiceDeskPaths } from '../../lib/paths' -import { UserProfileResult } from './User.loader' import { m } from '../../lib/messages' -import { useUpdateUserProfileMutation } from './User.generated' +import { + GetAdminNotificationsQuery, + useUpdateUserProfileMutation, +} from './User.generated' import { UpdateUserProfileInput } from '@island.is/api/schema' import React from 'react' +import { isValidDate } from '@island.is/shared/utils' +import { useGetAdminNotificationsQuery } from './User.generated' +import { UserProfileResult } from './User.loader' +import { Problem } from '@island.is/react-spa/shared' + +const DEFAULT_PAGE_SIZE = 10 const User = () => { const { formatMessage } = useLocale() @@ -22,6 +38,18 @@ const User = () => { const [updateProfile] = useUpdateUserProfileMutation() const { revalidate } = useRevalidator() + const { + data: notifications, + loading, + error, + fetchMore, + } = useGetAdminNotificationsQuery({ + variables: { + nationalId: user.nationalId, + input: { limit: DEFAULT_PAGE_SIZE }, + }, + }) + const handleUpdateProfile = async (input: UpdateUserProfileInput) => { try { const updatedProfile = await updateProfile({ @@ -39,6 +67,38 @@ const User = () => { } } + const loadMore = async () => { + if ( + loading || + !notifications || + !notifications?.adminNotifications?.pageInfo.hasNextPage + ) { + return + } + + await fetchMore({ + variables: { + nationalId: user.nationalId, + input: { + limit: DEFAULT_PAGE_SIZE, + after: + notifications?.adminNotifications?.pageInfo.endCursor ?? undefined, + }, + }, + updateQuery: (prev, { fetchMoreResult }): GetAdminNotificationsQuery => { + return { + adminNotifications: { + ...fetchMoreResult.adminNotifications, + data: [ + ...(prev.adminNotifications?.data || []), + ...(fetchMoreResult.adminNotifications?.data || []), + ], + } as GetAdminNotificationsQuery['adminNotifications'], + } + }, + }) + } + return ( { + + {formatMessage(m.notifications)} + {error ? ( + + ) : loading ? ( + + ) : ( + + + + } + > + + + + ID + Message ID + Sender ID + Sent + + + + {notifications?.adminNotifications?.data.map( + (notification, index) => ( + + {notification.id} + {notification.notificationId} + {notification.sender.id} + + {notification.sent && + isValidDate(new Date(notification.sent)) + ? format(new Date(notification.sent), 'dd.MM.yyyy') + : ''} + + + ), + )} + + + + )} + ) } diff --git a/libs/portals/admin/service-desk/src/screens/Users/Users.action.ts b/libs/portals/admin/service-desk/src/screens/Users/Users.action.ts index 13454e204aaa..bfe8cbfb1104 100644 --- a/libs/portals/admin/service-desk/src/screens/Users/Users.action.ts +++ b/libs/portals/admin/service-desk/src/screens/Users/Users.action.ts @@ -1,23 +1,20 @@ import { z } from 'zod' -import { redirect } from 'react-router-dom' import { RawRouterActionResponse, WrappedActionFn, } from '@island.is/portals/core' import { - replaceParams, validateFormData, ValidateFormDataResult, } from '@island.is/react-spa/shared' -import { maskString, isSearchTermValid } from '@island.is/shared/utils' +import { isSearchTermValid } from '@island.is/shared/utils' import { GetPaginatedUserProfilesDocument, GetPaginatedUserProfilesQuery, type GetPaginatedUserProfilesQueryVariables, } from './Users.generated' -import { ServiceDeskPaths } from '../../lib/paths' export enum ErrorType { // Add more error types here when needed