import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { combineLatest, Subject, switchMap, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policies/project-service-accounts-access-policies.view"; import { ApItemValueType, convertToProjectServiceAccountsAccessPoliciesView, } from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type"; import { ApItemViewType, convertPotentialGranteesToApItemViewType, convertProjectServiceAccountsViewToApItemViews, } from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type"; import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; @Component({ selector: "sm-project-service-accounts", templateUrl: "./project-service-accounts.component.html", }) export class ProjectServiceAccountsComponent implements OnInit, OnDestroy { private currentAccessPolicies: ApItemViewType[]; private destroy$ = new Subject(); private organizationId: string; private projectId: string; private currentAccessPolicies$ = combineLatest([this.route.params]).pipe( switchMap(([params]) => this.accessPolicyService .getProjectServiceAccountsAccessPolicies(params.organizationId, params.projectId) .then((policies) => { return convertProjectServiceAccountsViewToApItemViews(policies); }), ), ); private potentialGrantees$ = combineLatest([this.route.params]).pipe( switchMap(([params]) => this.accessPolicyService .getServiceAccountsPotentialGrantees(params.organizationId) .then((grantees) => { return convertPotentialGranteesToApItemViewType(grantees); }), ), ); protected formGroup = new FormGroup({ accessPolicies: new FormControl([] as ApItemValueType[]), }); protected loading = true; protected potentialGrantees: ApItemViewType[]; protected items: ApItemViewType[]; constructor( private route: ActivatedRoute, private changeDetectorRef: ChangeDetectorRef, private validationService: ValidationService, private accessPolicyService: AccessPolicyService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, ) {} ngOnInit(): void { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { this.organizationId = params.organizationId; this.projectId = params.projectId; }); combineLatest([this.potentialGrantees$, this.currentAccessPolicies$]) .pipe(takeUntil(this.destroy$)) .subscribe(([potentialGrantees, currentAccessPolicies]) => { this.potentialGrantees = potentialGrantees; this.items = this.getItems(potentialGrantees, currentAccessPolicies); this.setSelected(currentAccessPolicies); }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } submit = async () => { if (this.isFormInvalid()) { return; } const formValues = this.formGroup.value.accessPolicies; this.formGroup.disable(); try { const accessPoliciesView = await this.updateProjectServiceAccountsAccessPolicies( this.organizationId, this.projectId, formValues, ); const updatedView = convertProjectServiceAccountsViewToApItemViews(accessPoliciesView); this.items = this.getItems(this.potentialGrantees, updatedView); this.setSelected(updatedView); this.platformUtilsService.showToast( "success", null, this.i18nService.t("projectAccessUpdated"), ); } catch (e) { this.validationService.showError(e); this.setSelected(this.currentAccessPolicies); } this.formGroup.enable(); }; private setSelected(policiesToSelect: ApItemViewType[]) { this.loading = true; this.currentAccessPolicies = policiesToSelect; if (policiesToSelect != undefined) { // Must detect changes so that AccessSelector @Inputs() are aware of the latest // potentialGrantees, otherwise no selected values will be patched below this.changeDetectorRef.detectChanges(); this.formGroup.patchValue({ accessPolicies: policiesToSelect.map((m) => ({ type: m.type, id: m.id, permission: m.permission, })), }); } this.loading = false; } private isFormInvalid(): boolean { this.formGroup.markAllAsTouched(); return this.formGroup.invalid; } private async updateProjectServiceAccountsAccessPolicies( organizationId: string, projectId: string, selectedPolicies: ApItemValueType[], ): Promise { const view = convertToProjectServiceAccountsAccessPoliciesView(projectId, selectedPolicies); return await this.accessPolicyService.putProjectServiceAccountsAccessPolicies( organizationId, projectId, view, ); } private getItems(potentialGrantees: ApItemViewType[], currentAccessPolicies: ApItemViewType[]) { // If the user doesn't have access to the service account, they won't be in the potentialGrantees list. // Add them to the potentialGrantees list if they are selected. const items = [...potentialGrantees]; for (const policy of currentAccessPolicies) { const exists = potentialGrantees.some((grantee) => grantee.id === policy.id); if (!exists) { items.push(policy); } } return items; } }