• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alkem-io / server / #7980

11 Aug 2024 06:30AM UTC coverage: 13.774%. First build
#7980

Pull #4388

travis-ci

Pull Request #4388: Feature account-spaces: Direct linkage to account for user + organization

79 of 4128 branches covered (1.91%)

Branch coverage included in aggregate %.

4 of 34 new or added lines in 7 files covered. (11.76%)

1914 of 10341 relevant lines covered (18.51%)

3.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

14.43
/src/domain/community/organization/organization.service.ts
1
import { Inject, Injectable, LoggerService } from '@nestjs/common';
20✔
2
import { InjectRepository } from '@nestjs/typeorm';
20✔
3
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
20✔
4
import { FindOneOptions, Repository } from 'typeorm';
20✔
5
import {
20✔
6
  EntityNotFoundException,
7
  EntityNotInitializedException,
8
  ForbiddenException,
9
  RelationshipNotFoundException,
10
  ValidationException,
11
} from '@common/exceptions';
20✔
12
import { LogContext, ProfileType } from '@common/enums';
13
import { ProfileService } from '@domain/common/profile/profile.service';
14
import { UserGroupService } from '@domain/community/user-group/user-group.service';
15
import {
16
  CreateOrganizationInput,
20✔
17
  DeleteOrganizationInput,
20✔
18
  UpdateOrganizationInput,
19
} from '@domain/community/organization/dto';
20
import { IUserGroup } from '@domain/community/user-group';
21
import { AuthorizationPolicy } from '@domain/common/authorization-policy';
22
import { IAgent } from '@domain/agent/agent';
23
import { AgentService } from '@domain/agent/agent/agent.service';
24
import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service';
20✔
25
import { OrganizationVerificationService } from '../organization-verification/organization.verification.service';
20✔
26
import { IOrganizationVerification } from '../organization-verification/organization.verification.interface';
27
import { NVP } from '@domain/common/nvp/nvp.entity';
20✔
28
import { INVP } from '@domain/common/nvp/nvp.interface';
20✔
29
import { AgentInfo } from '@core/authentication.agent.info/agent.info';
30
import { limitAndShuffle } from '@common/utils/limitAndShuffle';
20✔
31
import { PaginationArgs } from '@core/pagination';
32
import { OrganizationFilterInput } from '@core/filtering';
20✔
33
import { IPaginatedType } from '@core/pagination/paginated.type';
34
import { getPaginationResults } from '@core/pagination/pagination.fn';
35
import { CreateUserGroupInput } from '../user-group/dto/user-group.dto.create';
20✔
36
import { ContributorQueryArgs } from '../contributor/dto/contributor.query.args';
37
import { Organization } from './organization.entity';
20✔
38
import { IOrganization } from './organization.interface';
20✔
39
import { TagsetReservedName } from '@common/enums/tagset.reserved.name';
20✔
40
import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface';
41
import { StorageAggregatorService } from '@domain/storage/storage-aggregator/storage.aggregator.service';
42
import { applyOrganizationFilter } from '@core/filtering/filters/organizationFilter';
43
import { NamingService } from '@services/infrastructure/naming/naming.service';
20✔
44
import { IAccount } from '@domain/space/account/account.interface';
45
import { StorageAggregatorType } from '@common/enums/storage.aggregator.type';
46
import { AgentType } from '@common/enums/agent.type';
20✔
47
import { ContributorService } from '../contributor/contributor.service';
48
import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type';
20✔
49
import { AccountType } from '@common/enums/account.type';
20✔
50
import { OrganizationVerificationEnum } from '@common/enums/organization.verification';
20✔
51
import { IOrganizationSettings } from '../organization-settings/organization.settings.interface';
52
import { OrganizationSettingsService } from '../organization-settings/organization.settings.service';
20✔
53
import { UpdateOrganizationSettingsEntityInput } from '../organization-settings/dto/organization.settings.dto.update';
20✔
54
import { AccountLookupService } from '@domain/space/account.lookup/account.lookup.service';
55
import { AccountHostService } from '@domain/space/account.host/account.host.service';
20✔
56
import { IRoleSet } from '@domain/access/role-set/role.set.interface';
20✔
57
import { RoleSetService } from '@domain/access/role-set/role.set.service';
20✔
58
import { organizationRoleDefinitions } from './definitions/organization.role.definitions';
59
import { CreateRoleSetInput } from '@domain/access/role-set/dto/role.set.dto.create';
20✔
60
import { RoleName } from '@common/enums/role.name';
20✔
61
import { organizationApplicationForm } from './definitions/organization.role.application.form';
62
import { RoleSetContributorType } from '@common/enums/role.set.contributor.type';
63
import { RoleSetType } from '@common/enums/role.set.type';
20✔
64
import { CreateReferenceInput } from '@domain/common/reference';
65
import { contributorDefaults } from '../contributor/contributor.defaults';
1✔
66

1✔
67
@Injectable()
1✔
68
export class OrganizationService {
1✔
69
  constructor(
1✔
70
    private accountLookupService: AccountLookupService,
1✔
71
    private accountHostService: AccountHostService,
1✔
72
    private authorizationPolicyService: AuthorizationPolicyService,
1✔
73
    private organizationVerificationService: OrganizationVerificationService,
1✔
74
    private organizationSettingsService: OrganizationSettingsService,
1✔
75
    private agentService: AgentService,
76
    private userGroupService: UserGroupService,
1✔
77
    private profileService: ProfileService,
1✔
78
    private namingService: NamingService,
79
    private storageAggregatorService: StorageAggregatorService,
80
    private contributorService: ContributorService,
81
    private roleSetService: RoleSetService,
82
    @InjectRepository(Organization)
83
    private organizationRepository: Repository<Organization>,
84
    @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService
×
85
  ) {}
86

×
87
  async createOrganization(
×
88
    organizationData: CreateOrganizationInput,
89
    agentInfo?: AgentInfo
×
90
  ): Promise<IOrganization> {
×
91
    if (organizationData.nameID) {
92
      // Convert nameID to lower case
93
      organizationData.nameID = organizationData.nameID.toLowerCase();
×
94
      await this.checkNameIdOrFail(organizationData.nameID);
×
95
    } else {
96
      organizationData.nameID = await this.createOrganizationNameID(
97
        organizationData.profileData?.displayName
×
98
      );
×
99
    }
100
    await this.checkDisplayNameOrFail(
×
101
      organizationData.profileData?.displayName
102
    );
103

104
    let organization: IOrganization = Organization.create(organizationData);
×
105
    organization.authorization = new AuthorizationPolicy(
106
      AuthorizationPolicyType.ORGANIZATION
107
    );
108

109
    const roleSetInput: CreateRoleSetInput = {
×
110
      roles: organizationRoleDefinitions,
111
      applicationForm: organizationApplicationForm,
112
      entryRoleName: RoleName.ASSOCIATE,
113
      type: RoleSetType.ORGANIZATION,
×
114
    };
115
    organization.roleSet =
116
      await this.roleSetService.createRoleSet(roleSetInput);
117
    organization.settings = this.getDefaultOrganizationSettings();
118

×
119
    organization.storageAggregator =
×
120
      await this.storageAggregatorService.createStorageAggregator(
×
121
        StorageAggregatorType.ORGANIZATION
122
      );
123

124
    organizationData.profileData.referencesData =
125
      this.getDefaultContributorProfileReferences();
×
126

127
    organization.profile = await this.profileService.createProfile(
128
      organizationData.profileData,
129
      ProfileType.ORGANIZATION,
130
      organization.storageAggregator
131
    );
×
132
    await this.profileService.addOrUpdateTagsetOnProfile(organization.profile, {
133
      name: TagsetReservedName.KEYWORDS,
×
134
      tags: [],
135
    });
136
    await this.profileService.addOrUpdateTagsetOnProfile(organization.profile, {
137
      name: TagsetReservedName.CAPABILITIES,
×
138
      tags: [],
×
139
    });
140

141
    this.contributorService.addAvatarVisualToContributorProfile(
142
      organization.profile,
×
143
      organizationData.profileData,
144
      agentInfo,
145
      organizationData.profileData.displayName
146
    );
147

×
148
    organization.groups = [];
×
149

150
    organization.agent = await this.agentService.createAgent({
151
      type: AgentType.ORGANIZATION,
152
    });
153
    const account = await this.accountHostService.createAccount(
×
154
      AccountType.ORGANIZATION
155
    );
156
    organization.accountID = account.id;
157

158
    // Cache some of the contents before saving
159
    const roleSetBeforeSave = organization.roleSet;
160

×
161
    organization = await this.save(organization);
162
    this.logger.verbose?.(
163
      `Created new organization with id ${organization.id}`,
164
      LogContext.COMMUNITY
165
    );
NEW
166
    organization.verification =
×
167
      await this.organizationVerificationService.createOrganizationVerification(
168
        { organizationID: organization.id }
NEW
169
      );
×
170
    // Ensure the credentials have the right resourceID
NEW
171
    organization.roleSet = await this.roleSetService.updateRoleResourceID(
×
172
      roleSetBeforeSave,
173
      organization.id
×
174
    );
175
    organization = await this.save(organization);
176

177
    organization = await this.getOrganizationOrFail(organization.id, {
×
178
      relations: {
179
        roleSet: true,
180
        profile: true,
×
181
      },
×
182
    });
183
    if (!organization.roleSet || !organization.profile) {
184
      throw new RelationshipNotFoundException(
185
        `Unable to load roleSet during org creation: ${organization.id}`,
186
        LogContext.COMMUNITY
187
      );
188
    }
189

190
    // Assign the creating agent as both a member and admin
191
    if (agentInfo) {
×
192
      await this.roleSetService.assignUserToRole(
×
193
        organization.roleSet,
194
        RoleName.ASSOCIATE,
×
195
        agentInfo.userID,
×
196
        agentInfo,
197
        false
×
198
      );
199

200
      await this.roleSetService.assignUserToRole(
201
        organization.roleSet,
202
        RoleName.ADMIN,
×
203
        agentInfo.userID,
×
204
        agentInfo,
205
        false
206
      );
207
    }
208

209
    const userID = agentInfo ? agentInfo.userID : '';
210
    await this.contributorService.ensureAvatarIsStoredInLocalStorageBucket(
211
      organization.profile.id,
212
      userID
×
213
    );
214

215
    return await this.getOrganizationOrFail(organization.id);
216
  }
×
217

×
218
  public async getRoleSet(organization: IOrganization): Promise<IRoleSet> {
219
    const organizationWithRoleSet = await this.getOrganizationOrFail(
220
      organization.id,
221
      {
222
        relations: { roleSet: true },
×
223
      }
×
224
    );
225

226
    if (!organizationWithRoleSet.roleSet) {
227
      throw new EntityNotInitializedException(
228
        `Unable to locate RoleSet for organization: ${organization.id}`,
229
        LogContext.COMMUNITY
×
230
      );
×
231
    }
232
    return organizationWithRoleSet.roleSet;
233
  }
×
234

×
235
  private getDefaultOrganizationSettings(): IOrganizationSettings {
236
    const settings: IOrganizationSettings = {
237
      membership: {
×
238
        allowUsersMatchingDomainToJoin: false,
×
239
      },
240
      privacy: {
241
        // Note: not currently used but will be near term.
×
242
        contributionRolesPubliclyVisible: true,
×
243
      },
244
    };
245
    return settings;
×
246
  }
247

248
  async checkNameIdOrFail(nameID: string) {
249
    const organizationCount = await this.organizationRepository.countBy({
250
      nameID: nameID,
251
    });
×
252
    if (organizationCount >= 1)
×
253
      throw new ValidationException(
254
        `Organization: the provided nameID is already taken: ${nameID}`,
255
        LogContext.COMMUNITY
256
      );
257
  }
258

259
  async checkDisplayNameOrFail(
260
    newDisplayName?: string,
261
    existingDisplayName?: string
262
  ) {
263
    if (!newDisplayName) {
NEW
264
      return;
×
265
    }
266
    if (newDisplayName === existingDisplayName) {
267
      return;
×
268
    }
×
269
    const organizationCount = await this.organizationRepository.countBy({
270
      profile: {
271
        displayName: newDisplayName,
272
      },
273
    });
274
    if (organizationCount >= 1)
×
275
      throw new ValidationException(
276
        `Organization: the provided displayName is already taken: ${newDisplayName}`,
×
277
        LogContext.COMMUNITY
×
278
      );
279
  }
280

×
281
  public async updateOrganizationSettings(
×
282
    organization: IOrganization,
283
    settingsData: UpdateOrganizationSettingsEntityInput
284
  ): Promise<IOrganization> {
285
    organization.settings = this.organizationSettingsService.updateSettings(
286
      organization.settings,
×
287
      settingsData
×
288
    );
×
289
    return await this.save(organization);
290
  }
291

292
  async updateOrganization(
293
    organizationData: UpdateOrganizationInput
294
  ): Promise<IOrganization> {
×
295
    const organization = await this.getOrganizationOrFail(organizationData.ID, {
×
296
      relations: { profile: true },
297
    });
298

×
299
    await this.checkDisplayNameOrFail(
×
300
      organizationData.profileData?.displayName,
301
      organization.profile.displayName
302
    );
×
303

×
304
    // Check the tagsets
305
    if (organizationData.profileData && organization.profile) {
306
      organization.profile = await this.profileService.updateProfile(
307
        organization.profile,
308
        organizationData.profileData
×
309
      );
×
310
    }
311

312
    if (organizationData.legalEntityName !== undefined) {
313
      organization.legalEntityName = organizationData.legalEntityName;
314
    }
×
315

316
    if (organizationData.domain !== undefined) {
317
      organization.domain = organizationData.domain;
×
318
    }
×
319

320
    if (organizationData.website !== undefined) {
321
      organization.website = organizationData.website;
322
    }
323

324
    if (organizationData.contactEmail !== undefined) {
325
      organization.contactEmail = organizationData.contactEmail;
326
    }
×
327

×
328
    return await this.organizationRepository.save(organization);
329
  }
×
330

331
  async deleteOrganization(
332
    deleteData: DeleteOrganizationInput
333
  ): Promise<IOrganization> {
×
334
    const orgID = deleteData.ID;
335
    const organization = await this.getOrganizationOrFail(orgID, {
×
336
      relations: {
337
        profile: true,
338
        agent: true,
×
339
        verification: true,
340
        groups: true,
341
        storageAggregator: true,
342
        roleSet: true,
343
      },
344
    });
345

×
346
    if (
×
347
      !organization.roleSet ||
×
348
      !organization.profile ||
349
      !organization.verification ||
350
      !organization.agent
351
    ) {
×
352
      throw new RelationshipNotFoundException(
353
        `Unable to delete org, missing relations: ${organization.id}`,
354
        LogContext.COMMUNITY
NEW
355
      );
×
356
    }
357
    // TODO: give additional feedback?
358
    const accountHasResources =
359
      await this.accountLookupService.areResourcesInAccount(
360
        organization.accountID
361
      );
362
    if (accountHasResources) {
363
      throw new ForbiddenException(
×
364
        'Unable to delete Organization: account contain one or more resources',
365
        LogContext.SPACES
366
      );
367
    }
×
368

×
369
    await this.profileService.deleteProfile(organization.profile.id);
370

371
    if (organization.storageAggregator) {
372
      await this.storageAggregatorService.delete(
373
        organization.storageAggregator.id
×
374
      );
375
    }
376

377
    if (organization.groups) {
×
378
      for (const group of organization.groups) {
×
379
        await this.userGroupService.removeUserGroup({
×
380
          ID: group.id,
381
        });
382
      }
383
    }
384

×
385
    if (organization.authorization) {
×
386
      await this.authorizationPolicyService.delete(organization.authorization);
×
387
    }
×
388

389
    await this.agentService.deleteAgent(organization.agent.id);
390

391
    await this.organizationVerificationService.delete(
392
      organization.verification.id
393
    );
394

395
    await this.roleSetService.removeRoleSetOrFail(organization.roleSet.id);
396

397
    const result = await this.organizationRepository.remove(
×
398
      organization as Organization
399
    );
400
    result.id = orgID;
×
401
    return result;
402
  }
403

404
  async getOrganization(
405
    organizationID: string,
406
    options?: FindOneOptions<Organization>
407
  ): Promise<IOrganization | null> {
×
408
    const organization = await this.organizationRepository.findOne({
×
409
      ...options,
×
410
      where: { ...options?.where, id: organizationID },
411
    });
412

×
413
    return organization;
414
  }
415

416
  async getOrganizationOrFail(
417
    organizationID: string,
418
    options?: FindOneOptions<Organization>
419
  ): Promise<IOrganization | never> {
×
420
    const organization = await this.getOrganization(organizationID, options);
421
    if (!organization)
422
      throw new EntityNotFoundException(
423
        `Unable to find Organization with ID: ${organizationID}`,
×
424
        LogContext.COMMUNITY
425
      );
×
426
    return organization;
×
427
  }
428

×
429
  public async getAccount(organization: IOrganization): Promise<IAccount> {
×
430
    return await this.accountLookupService.getAccountOrFail(
431
      organization.accountID
×
432
    );
×
433
  }
434

435
  async getOrganizations(args: ContributorQueryArgs): Promise<IOrganization[]> {
×
436
    const limit = args.limit;
437
    const shuffle = args.shuffle || false;
438
    this.logger.verbose?.(
439
      `Querying all organizations with limit: ${limit} and shuffle: ${shuffle}`,
440
      LogContext.COMMUNITY
×
441
    );
442

443
    const credentialsFilter = args.filter?.credentials;
444
    let organizations: IOrganization[] = [];
×
445
    if (credentialsFilter) {
446
      organizations = await this.organizationRepository
447
        .createQueryBuilder('organization')
×
448
        .leftJoinAndSelect('organization.agent', 'agent')
×
449
        .leftJoinAndSelect('agent.credentials', 'credential')
×
450
        .where('credential.type IN (:credentialsFilter)')
×
451
        .setParameters({
452
          credentialsFilter: credentialsFilter,
×
453
        })
454
        .getMany();
455
    } else {
456
      organizations = await this.organizationRepository.find();
×
457
    }
×
458

459
    return limitAndShuffle(organizations, limit, shuffle);
×
460
  }
461

462
  async getPaginatedOrganizations(
463
    paginationArgs: PaginationArgs,
×
464
    filter?: OrganizationFilterInput,
465
    status?: OrganizationVerificationEnum
466
  ): Promise<IPaginatedType<IOrganization>> {
467
    const qb = this.organizationRepository.createQueryBuilder('organization');
468
    qb.leftJoinAndSelect('organization.authorization', 'authorization_policy');
469

470
    if (status) {
471
      qb.leftJoin('organization.verification', 'verification').where(
472
        'verification.status = :status',
×
473
        { status: status }
×
474
      );
475
    }
476

477
    if (filter) {
478
      applyOrganizationFilter(qb, filter);
×
479
    }
480

481
    return getPaginationResults(qb, paginationArgs);
482
  }
483

×
484
  async getMetrics(organization: IOrganization): Promise<INVP[]> {
485
    const activity: INVP[] = [];
×
486
    const roleSet = await this.getRoleSet(organization);
487

488
    const membersCount = await this.roleSetService.countContributorsPerRole(
489
      roleSet,
×
490
      RoleName.ASSOCIATE,
491
      RoleSetContributorType.USER
492
    );
493
    const membersTopic = new NVP('associates', membersCount.toString());
×
494
    membersTopic.id = `associates-${organization.id}`;
495
    activity.push(membersTopic);
496

497
    return activity;
498
  }
499

×
500
  async createGroup(groupData: CreateUserGroupInput): Promise<IUserGroup> {
×
501
    const orgID = groupData.parentID;
×
502
    const groupName = groupData.profile.displayName;
503
    // First find the Challenge
504
    this.logger.verbose?.(
505
      `Adding userGroup (${groupName}) to organization (${orgID})`
506
    );
×
507
    // Try to find the organization
508
    const organization = await this.getOrganizationOrFail(orgID, {
509
      relations: {
510
        groups: {
×
511
          profile: true,
512
        },
513
        storageAggregator: true,
514
      },
515
    });
516

517
    if (!organization.storageAggregator) {
518
      throw new EntityNotInitializedException(
519
        `Organization StorageAggregator not initialized: ${organization.id}`,
520
        LogContext.COMMUNITY
×
521
      );
×
522
    }
×
523
    const group = await this.userGroupService.addGroupWithName(
524
      organization,
525
      groupName,
526
      organization.storageAggregator
×
527
    );
528
    await this.organizationRepository.save(organization);
529

530
    return group;
×
531
  }
532

533
  async save(organization: IOrganization): Promise<IOrganization> {
534
    return await this.organizationRepository.save(organization);
535
  }
536

537
  async getAgent(organization: IOrganization): Promise<IAgent> {
×
538
    const organizationWithAgent = await this.getOrganizationOrFail(
539
      organization.id,
×
540
      {
×
541
        relations: { agent: true },
542
      }
543
    );
544
    const agent = organizationWithAgent.agent;
545
    if (!agent)
546
      throw new EntityNotInitializedException(
×
547
        `User Agent not initialized: ${organization.id}`,
548
        LogContext.AUTH
549
      );
550

551
    return agent;
552
  }
×
553

554
  async getUserGroups(organization: IOrganization): Promise<IUserGroup[]> {
555
    const organizationGroups = await this.getOrganizationOrFail(
556
      organization.id,
557
      {
558
        relations: {
559
          groups: {
560
            profile: true,
561
          },
×
562
        },
563
      }
×
564
    );
×
565
    const groups = organizationGroups.groups;
566
    if (!groups)
567
      throw new ValidationException(
568
        `No groups on organization: ${organization.id}`,
569
        LogContext.COMMUNITY
570
      );
×
571
    return groups;
572
  }
573

574
  async getStorageAggregatorOrFail(
575
    organizationID: string
576
  ): Promise<IStorageAggregator> {
×
577
    const organizationWithStorageAggregator = await this.getOrganizationOrFail(
×
578
      organizationID,
579
      {
580
        relations: {
581
          storageAggregator: true,
582
        },
583
      }
584
    );
585
    const storageAggregator =
586
      organizationWithStorageAggregator.storageAggregator;
587

588
    if (!storageAggregator) {
589
      throw new EntityNotFoundException(
590
        `Unable to find storageAggregator for Organization with nameID: ${organizationWithStorageAggregator.id}`,
×
591
        LogContext.COMMUNITY
×
592
      );
×
593
    }
594

595
    return storageAggregator;
×
596
  }
597

×
598
  async getVerification(
599
    organizationParent: IOrganization
600
  ): Promise<IOrganizationVerification> {
601
    const organization = await this.getOrganizationOrFail(
602
      organizationParent.id,
603
      {
×
604
        relations: { verification: true },
×
605
      }
606
    );
607
    if (!organization.verification) {
608
      throw new EntityNotFoundException(
609
        `Unable to load verification for organisation: ${organization.id}`,
610
        LogContext.COMMUNITY
611
      );
612
    }
613
    return organization.verification;
614
  }
615

616
  public async createOrganizationNameID(displayName: string): Promise<string> {
×
617
    const base = `${displayName}`;
618
    const reservedNameIDs =
619
      await this.namingService.getReservedNameIDsInOrganizations(); // This will need to be smarter later
620
    return this.namingService.createNameIdAvoidingReservedNameIDs(
621
      base,
622
      reservedNameIDs
×
623
    );
624
  }
625

626
  private getDefaultContributorProfileReferences(): CreateReferenceInput[] {
627
    const references: CreateReferenceInput[] = [];
628
    const referenceTemplates = contributorDefaults.references;
×
629

×
630
    if (referenceTemplates) {
631
      for (const referenceTemplate of referenceTemplates) {
632
        references.push({
633
          name: referenceTemplate.name,
634
          uri: referenceTemplate.uri,
×
635
          description: referenceTemplate.description,
636
        });
637
      }
638
    }
×
639

×
640
    return references;
641
  }
642
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc