diff --git a/src/core/authorization/authorization.service.ts b/src/core/authorization/authorization.service.ts index 402d67f4d4..c765bcaa03 100644 --- a/src/core/authorization/authorization.service.ts +++ b/src/core/authorization/authorization.service.ts @@ -89,7 +89,7 @@ export class AuthorizationService { ); if (authorization.credentialRules === '') { throw new AuthorizationInvalidPolicyException( - `AuthorizationPolicy without credential rules provided: ${authorization.id}`, + `AuthorizationPolicy without credential rules provided: ${authorization.id}, type: ${authorization.type}`, LogContext.AUTH ); } @@ -130,7 +130,7 @@ export class AuthorizationService { for (const privilege of rule.grantedPrivileges) { if (privilege === privilegeRequired) { this.logger.verbose?.( - `[CredentialRule] Granted privilege '${privilegeRequired}' using rule '${rule.name}' on authorization ${authorization.id}`, + `[CredentialRule] Granted privilege '${privilegeRequired}' using rule '${rule.name}' on authorization ${authorization.id} on type: ${authorization.type}`, LogContext.AUTH_POLICY ); return true; diff --git a/src/domain/access/invitation/dto/invitation.dto.create.ts b/src/domain/access/invitation/dto/invitation.dto.create.ts index cde00875f8..bb99b58532 100644 --- a/src/domain/access/invitation/dto/invitation.dto.create.ts +++ b/src/domain/access/invitation/dto/invitation.dto.create.ts @@ -1,4 +1,9 @@ -import { MID_TEXT_LENGTH, UUID_LENGTH } from '@common/constants'; +import { + MID_TEXT_LENGTH, + SMALL_TEXT_LENGTH, + UUID_LENGTH, +} from '@common/constants'; +import { CommunityRoleType } from '@common/enums/community.role'; import { UUID } from '@domain/common/scalars'; import { Field, InputType } from '@nestjs/graphql'; import { IsOptional, MaxLength } from 'class-validator'; @@ -7,11 +12,11 @@ import { IsOptional, MaxLength } from 'class-validator'; export class CreateInvitationInput { @Field(() => UUID, { nullable: false, - description: 'The identifier for the contributor being invited.', + description: + 'The identifier for the contributor being invited to join in the entry Role.', }) - @IsOptional() @MaxLength(UUID_LENGTH) - invitedContributor!: string; + invitedContributorID!: string; @Field({ nullable: true }) @IsOptional() @@ -22,4 +27,13 @@ export class CreateInvitationInput { roleSetID!: string; invitedToParent!: boolean; + + @Field(() => CommunityRoleType, { + nullable: true, + description: + 'An additional role to assign to the Contributor, in addition to the entry Role.', + }) + @IsOptional() + @MaxLength(SMALL_TEXT_LENGTH) + extraRole?: CommunityRoleType; } diff --git a/src/domain/access/invitation/invitation.entity.ts b/src/domain/access/invitation/invitation.entity.ts index 5983600a6b..e2931f9111 100644 --- a/src/domain/access/invitation/invitation.entity.ts +++ b/src/domain/access/invitation/invitation.entity.ts @@ -5,6 +5,7 @@ import { AuthorizableEntity } from '@domain/common/entity/authorizable-entity'; import { CommunityContributorType } from '@common/enums/community.contributor.type'; import { ENUM_LENGTH, MID_TEXT_LENGTH, UUID_LENGTH } from '@common/constants'; import { RoleSet } from '@domain/access/role-set/role.set.entity'; +import { CommunityRoleType } from '@common/enums/community.role'; @Entity() export class Invitation extends AuthorizableEntity implements IInvitation { // todo ID in migration is varchar - must be char(36) @@ -17,7 +18,7 @@ export class Invitation extends AuthorizableEntity implements IInvitation { lifecycle!: Lifecycle; @Column('char', { length: UUID_LENGTH, nullable: false }) - invitedContributor!: string; + invitedContributorID!: string; @Column('char', { length: UUID_LENGTH, nullable: false }) createdBy!: string; @@ -37,4 +38,10 @@ export class Invitation extends AuthorizableEntity implements IInvitation { onDelete: 'CASCADE', }) roleSet?: RoleSet; + + @Column('varchar', { + length: ENUM_LENGTH, + nullable: true, + }) + extraRole?: CommunityRoleType; } diff --git a/src/domain/access/invitation/invitation.interface.ts b/src/domain/access/invitation/invitation.interface.ts index b550ad6473..b9674421ba 100644 --- a/src/domain/access/invitation/invitation.interface.ts +++ b/src/domain/access/invitation/invitation.interface.ts @@ -3,10 +3,11 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { IAuthorizable } from '@domain/common/entity/authorizable-entity'; import { CommunityContributorType } from '@common/enums/community.contributor.type'; import { IRoleSet } from '@domain/access/role-set'; +import { CommunityRoleType } from '@common/enums/community.role'; @ObjectType('Invitation') export class IInvitation extends IAuthorizable { - invitedContributor!: string; + invitedContributorID!: string; createdBy!: string; roleSet?: IRoleSet; @@ -35,4 +36,11 @@ export class IInvitation extends IAuthorizable { description: 'The type of contributor that is invited.', }) contributorType!: CommunityContributorType; + + @Field(() => CommunityRoleType, { + nullable: true, + description: + 'An additional role to assign to the Contributor, in addition to the entry Role.', + }) + extraRole?: CommunityRoleType; } diff --git a/src/domain/access/invitation/invitation.service.ts b/src/domain/access/invitation/invitation.service.ts index fbf1b35599..a17c231f13 100644 --- a/src/domain/access/invitation/invitation.service.ts +++ b/src/domain/access/invitation/invitation.service.ts @@ -111,7 +111,7 @@ export class InvitationService { async getInvitedContributor(invitation: IInvitation): Promise { const contributor = await this.contributorService.getContributorByUuidOrFail( - invitation.invitedContributor + invitation.invitedContributorID ); if (!contributor) throw new RelationshipNotFoundException( @@ -137,7 +137,7 @@ export class InvitationService { ): Promise { const existingInvitations = await this.invitationRepository.find({ where: { - invitedContributor: contributorID, + invitedContributorID: contributorID, roleSet: { id: roleSetID }, }, relations: { roleSet: true }, @@ -153,7 +153,7 @@ export class InvitationService { ): Promise { const findOpts: FindManyOptions = { relations: { roleSet: true }, - where: { invitedContributor: contributorID }, + where: { invitedContributorID: contributorID }, }; if (states.length) { diff --git a/src/domain/access/role-set/dto/role.set.dto.entry.role.invite.ts b/src/domain/access/role-set/dto/role.set.dto.entry.role.invite.ts index d6f89354ac..6bdeeb958e 100644 --- a/src/domain/access/role-set/dto/role.set.dto.entry.role.invite.ts +++ b/src/domain/access/role-set/dto/role.set.dto.entry.role.invite.ts @@ -1,7 +1,12 @@ import { Field, InputType } from '@nestjs/graphql'; import { UUID } from '@domain/common/scalars'; import { IsOptional, MaxLength } from 'class-validator'; -import { MID_TEXT_LENGTH, UUID_LENGTH } from '@common/constants'; +import { + MID_TEXT_LENGTH, + SMALL_TEXT_LENGTH, + UUID_LENGTH, +} from '@common/constants'; +import { CommunityRoleType } from '@common/enums/community.role'; @InputType() export class InviteForEntryRoleOnRoleSetInput { @@ -20,6 +25,15 @@ export class InviteForEntryRoleOnRoleSetInput { @MaxLength(MID_TEXT_LENGTH) welcomeMessage?: string; + @Field(() => CommunityRoleType, { + nullable: true, + description: + 'An additional role to assign to the Contributors, in addition to the entry Role.', + }) + @IsOptional() + @MaxLength(SMALL_TEXT_LENGTH) + extraRole?: CommunityRoleType; + createdBy!: string; invitedToParent!: boolean; diff --git a/src/domain/access/role-set/role.set.lifecycle.invitation.options.provider.ts b/src/domain/access/role-set/role.set.lifecycle.invitation.options.provider.ts index ead2b31f44..296e725cc6 100644 --- a/src/domain/access/role-set/role.set.lifecycle.invitation.options.provider.ts +++ b/src/domain/access/role-set/role.set.lifecycle.invitation.options.provider.ts @@ -94,7 +94,7 @@ export class RoleSetInvitationLifecycleOptionsProvider { }, } ); - const contributorID = invitation.invitedContributor; + const contributorID = invitation.invitedContributorID; const roleSet = invitation.roleSet; if (!contributorID || !roleSet) { throw new EntityNotInitializedException( @@ -127,6 +127,16 @@ export class RoleSetInvitationLifecycleOptionsProvider { event.agentInfo, true ); + if (invitation.extraRole) { + await this.roleSetService.assignContributorToRole( + roleSet, + invitation.extraRole, + contributorID, + invitation.contributorType, + event.agentInfo, + false + ); + } } finally { resolve(); } diff --git a/src/domain/access/role-set/role.set.resolver.mutations.ts b/src/domain/access/role-set/role.set.resolver.mutations.ts index 527f06f828..609be0bcb1 100644 --- a/src/domain/access/role-set/role.set.resolver.mutations.ts +++ b/src/domain/access/role-set/role.set.resolver.mutations.ts @@ -306,6 +306,7 @@ export class RoleSetResolverMutations { roleData.contributorID ); } + @UseGuards(GraphqlGuard) @Mutation(() => IRoleSet, { description: @@ -506,6 +507,7 @@ export class RoleSetResolverMutations { invitedContributor, agentInfo, invitationData.invitedToParent, + invitationData.extraRole, invitationData.welcomeMessage ); }) @@ -517,13 +519,15 @@ export class RoleSetResolverMutations { invitedContributor: IContributor, agentInfo: AgentInfo, invitedToParent: boolean, + extraRole?: CommunityRoleType, welcomeMessage?: string ): Promise { const input: CreateInvitationInput = { roleSetID: roleSet.id, - invitedContributor: invitedContributor.id, + invitedContributorID: invitedContributor.id, createdBy: agentInfo.userID, invitedToParent, + extraRole, welcomeMessage, }; diff --git a/src/domain/access/role-set/role.set.service.ts b/src/domain/access/role-set/role.set.service.ts index 54212e921e..918d0d8743 100644 --- a/src/domain/access/role-set/role.set.service.ts +++ b/src/domain/access/role-set/role.set.service.ts @@ -1023,7 +1023,7 @@ export class RoleSetService { ): Promise { const { contributor: contributor, agent } = await this.contributorService.getContributorAndAgent( - invitationData.invitedContributor + invitationData.invitedContributorID ); const roleSet = await this.getRoleSetOrFail(invitationData.roleSetID); @@ -1082,7 +1082,7 @@ export class RoleSetService { const openInvitation = await this.findOpenInvitation(user.id, roleSet.id); if (openInvitation) { throw new RoleSetMembershipException( - `Application not possible: An open invitation (ID: ${openInvitation.id}) already exists for contributor ${openInvitation.invitedContributor} (${openInvitation.contributorType}) on RoleSet: ${roleSet.id}.`, + `Application not possible: An open invitation (ID: ${openInvitation.id}) already exists for contributor ${openInvitation.invitedContributorID} (${openInvitation.contributorType}) on RoleSet: ${roleSet.id}.`, LogContext.COMMUNITY ); } @@ -1107,7 +1107,7 @@ export class RoleSetService { ); if (openInvitation) { throw new RoleSetMembershipException( - `Invitation not possible: An open invitation (ID: ${openInvitation.id}) already exists for contributor ${openInvitation.invitedContributor} (${openInvitation.contributorType}) on RoleSet: ${roleSet.id}.`, + `Invitation not possible: An open invitation (ID: ${openInvitation.id}) already exists for contributor ${openInvitation.invitedContributorID} (${openInvitation.contributorType}) on RoleSet: ${roleSet.id}.`, LogContext.COMMUNITY ); } diff --git a/src/domain/community/virtual-contributor/virtual.contributor.service.ts b/src/domain/community/virtual-contributor/virtual.contributor.service.ts index 42531756c4..45a3a8ca87 100644 --- a/src/domain/community/virtual-contributor/virtual.contributor.service.ts +++ b/src/domain/community/virtual-contributor/virtual.contributor.service.ts @@ -592,7 +592,7 @@ export class VirtualContributorService { //adding this to avoid circular dependency between VirtualContributor, Room, and Invitation private async deleteVCInvitations(contributorID: string) { const invitations = await this.entityManager.find(Invitation, { - where: { invitedContributor: contributorID }, + where: { invitedContributorID: contributorID }, }); for (const invitation of invitations) { if (invitation.authorization) { diff --git a/src/migrations/1727930288139-invitationToRole.ts b/src/migrations/1727930288139-invitationToRole.ts new file mode 100644 index 0000000000..0d25ed8ec3 --- /dev/null +++ b/src/migrations/1727930288139-invitationToRole.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InvitationToRole1727930288139 implements MigrationInterface { + name = 'InvitationToRole1727930288139'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`invitation\` ADD \`extraRole\` varchar(128) NULL` + ); + + await queryRunner.query( + `ALTER TABLE \`invitation\` CHANGE \`invitedContributor\` \`invitedContributorID\` char(36) NOT NULL` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`invitation\` DROP COLUMN \`extraRole\`` + ); + + await queryRunner.query( + `ALTER TABLE \`invitation\` CHANGE \`invitedContributorID\` \`invitedContributor\` char(36) NOT NULL` + ); + } +} diff --git a/src/services/api/registration/registration.service.ts b/src/services/api/registration/registration.service.ts index 76aefe4083..f17c244d6f 100644 --- a/src/services/api/registration/registration.service.ts +++ b/src/services/api/registration/registration.service.ts @@ -124,7 +124,7 @@ export class RegistrationService { // Process community invitations if (roleSet) { const invitationInput: CreateInvitationInput = { - invitedContributor: user.id, + invitedContributorID: user.id, roleSetID: roleSet.id, createdBy: platformInvitation.createdBy, invitedToParent: platformInvitation.communityInvitedToParent, diff --git a/src/services/api/roles/roles.service.ts b/src/services/api/roles/roles.service.ts index e1d64c146f..256b82c652 100644 --- a/src/services/api/roles/roles.service.ts +++ b/src/services/api/roles/roles.service.ts @@ -241,7 +241,7 @@ export class RolesService { invitation.createdDate, invitation.updatedDate ); - invitationResult.contributorID = invitation.invitedContributor; + invitationResult.contributorID = invitation.invitedContributorID; invitationResult.contributorType = invitation.contributorType; invitationResult.createdBy = invitation.createdBy ?? '';