mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
[AC-1218] Add ability to delete Provider Portals (#8685)
* initial commit * add changes from running prettier * resolve the linx issue * resolve the lint issue * resolving lint error * correct the redirect issue * resolve pr commit * Add a feature flag * move the new component to adminconsole * resolve some pr comments * move the endpoint from ApiService to providerApiService * move provider endpoints to the provider-api class * change the header * resolve some pr comments
This commit is contained in:
parent
4db383850f
commit
a72b7f3d21
@ -18,11 +18,13 @@ import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/
|
|||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
|
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||||
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
|
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
|
||||||
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
|
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
|
||||||
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||||
|
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||||
@ -232,6 +234,7 @@ export class Main {
|
|||||||
stateEventRunnerService: StateEventRunnerService;
|
stateEventRunnerService: StateEventRunnerService;
|
||||||
biometricStateService: BiometricStateService;
|
biometricStateService: BiometricStateService;
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService;
|
billingAccountProfileStateService: BillingAccountProfileStateService;
|
||||||
|
providerApiService: ProviderApiServiceAbstraction;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let p = null;
|
let p = null;
|
||||||
@ -692,6 +695,8 @@ export class Main {
|
|||||||
this.eventUploadService,
|
this.eventUploadService,
|
||||||
this.authService,
|
this.authService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.providerApiService = new ProviderApiService(this.apiService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||||
|
<div class="row justify-content-md-center mt-5">
|
||||||
|
<div class="col-5">
|
||||||
|
<p class="lead text-center mb-4">{{ "deleteProvider" | i18n }}</p>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<app-callout type="warning">{{ "deleteProviderWarning" | i18n }}</app-callout>
|
||||||
|
<p class="text-center">
|
||||||
|
<strong>{{ name }}</strong>
|
||||||
|
</p>
|
||||||
|
<p>{{ "deleteProviderRecoverConfirmDesc" | i18n }}</p>
|
||||||
|
<hr />
|
||||||
|
<div class="d-flex">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-danger btn-block btn-submit"
|
||||||
|
[disabled]="form.loading"
|
||||||
|
>
|
||||||
|
<span>{{ "deleteProvider" | i18n }}</span>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||||
|
{{ "cancel" | i18n }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,61 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||||
|
import { ProviderVerifyRecoverDeleteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-verify-recover-delete.request";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-verify-recover-delete-provider",
|
||||||
|
templateUrl: "verify-recover-delete-provider.component.html",
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
|
export class VerifyRecoverDeleteProviderComponent implements OnInit {
|
||||||
|
name: string;
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
|
||||||
|
private providerId: string;
|
||||||
|
private token: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private providerApiService: ProviderApiServiceAbstraction,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private logService: LogService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
const qParams = await firstValueFrom(this.route.queryParams);
|
||||||
|
if (qParams.providerId != null && qParams.token != null && qParams.name != null) {
|
||||||
|
this.providerId = qParams.providerId;
|
||||||
|
this.token = qParams.token;
|
||||||
|
this.name = qParams.name;
|
||||||
|
} else {
|
||||||
|
await this.router.navigate(["/"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
try {
|
||||||
|
const request = new ProviderVerifyRecoverDeleteRequest(this.token);
|
||||||
|
this.formPromise = this.providerApiService.providerRecoverDeleteToken(
|
||||||
|
this.providerId,
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
await this.formPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
this.i18nService.t("providerDeleted"),
|
||||||
|
this.i18nService.t("providerDeletedDesc"),
|
||||||
|
);
|
||||||
|
await this.router.navigate(["/"]);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/organization-create.request";
|
import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/organization-create.request";
|
||||||
@ -147,6 +148,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private providerApiService: ProviderApiServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
this.selfHosted = platformUtilsService.isSelfHost();
|
this.selfHosted = platformUtilsService.isSelfHost();
|
||||||
}
|
}
|
||||||
@ -182,7 +184,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||||||
if (this.hasProvider) {
|
if (this.hasProvider) {
|
||||||
this.formGroup.controls.businessOwned.setValue(true);
|
this.formGroup.controls.businessOwned.setValue(true);
|
||||||
this.changedOwnedBusiness();
|
this.changedOwnedBusiness();
|
||||||
this.provider = await this.apiService.getProvider(this.providerId);
|
this.provider = await this.providerApiService.getProvider(this.providerId);
|
||||||
const providerDefaultPlan = this.passwordManagerPlans.find(
|
const providerDefaultPlan = this.passwordManagerPlans.find(
|
||||||
(plan) => plan.type === PlanType.TeamsAnnually,
|
(plan) => plan.type === PlanType.TeamsAnnually,
|
||||||
);
|
);
|
||||||
|
@ -13,6 +13,7 @@ import { flagEnabled, Flags } from "../utils/flags";
|
|||||||
|
|
||||||
import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component";
|
import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component";
|
||||||
import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component";
|
import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component";
|
||||||
|
import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component";
|
||||||
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
||||||
import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component";
|
import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component";
|
||||||
import { AcceptOrganizationComponent } from "./auth/accept-organization.component";
|
import { AcceptOrganizationComponent } from "./auth/accept-organization.component";
|
||||||
@ -156,6 +157,12 @@ const routes: Routes = [
|
|||||||
canActivate: [UnauthGuard],
|
canActivate: [UnauthGuard],
|
||||||
data: { titleId: "deleteAccount" },
|
data: { titleId: "deleteAccount" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "verify-recover-delete-provider",
|
||||||
|
component: VerifyRecoverDeleteProviderComponent,
|
||||||
|
canActivate: [UnauthGuard],
|
||||||
|
data: { titleId: "deleteAccount" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "send/:sendId/:key",
|
path: "send/:sendId/:key",
|
||||||
component: AccessComponent,
|
component: AccessComponent,
|
||||||
|
@ -13,6 +13,7 @@ import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } f
|
|||||||
import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../admin-console/organizations/tools/unsecured-websites-report.component";
|
import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../admin-console/organizations/tools/unsecured-websites-report.component";
|
||||||
import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../admin-console/organizations/tools/weak-passwords-report.component";
|
import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../admin-console/organizations/tools/weak-passwords-report.component";
|
||||||
import { ProvidersComponent } from "../admin-console/providers/providers.component";
|
import { ProvidersComponent } from "../admin-console/providers/providers.component";
|
||||||
|
import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component";
|
||||||
import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component";
|
import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component";
|
||||||
import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component";
|
import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component";
|
||||||
import { AcceptOrganizationComponent } from "../auth/accept-organization.component";
|
import { AcceptOrganizationComponent } from "../auth/accept-organization.component";
|
||||||
@ -184,6 +185,7 @@ import { SharedModule } from "./shared.module";
|
|||||||
VerifyEmailComponent,
|
VerifyEmailComponent,
|
||||||
VerifyEmailTokenComponent,
|
VerifyEmailTokenComponent,
|
||||||
VerifyRecoverDeleteComponent,
|
VerifyRecoverDeleteComponent,
|
||||||
|
VerifyRecoverDeleteProviderComponent,
|
||||||
LowKdfComponent,
|
LowKdfComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
@ -261,6 +263,7 @@ import { SharedModule } from "./shared.module";
|
|||||||
VerifyEmailComponent,
|
VerifyEmailComponent,
|
||||||
VerifyEmailTokenComponent,
|
VerifyEmailTokenComponent,
|
||||||
VerifyRecoverDeleteComponent,
|
VerifyRecoverDeleteComponent,
|
||||||
|
VerifyRecoverDeleteProviderComponent,
|
||||||
LowKdfComponent,
|
LowKdfComponent,
|
||||||
HeaderModule,
|
HeaderModule,
|
||||||
DangerZoneComponent,
|
DangerZoneComponent,
|
||||||
|
@ -7908,5 +7908,41 @@
|
|||||||
},
|
},
|
||||||
"restrictedGroupAccessDesc": {
|
"restrictedGroupAccessDesc": {
|
||||||
"message": "You cannot add yourself to a group."
|
"message": "You cannot add yourself to a group."
|
||||||
|
},
|
||||||
|
"deleteProvider": {
|
||||||
|
"message": "Delete provider"
|
||||||
|
},
|
||||||
|
"deleteProviderConfirmation": {
|
||||||
|
"message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data."
|
||||||
|
},
|
||||||
|
"deleteProviderName": {
|
||||||
|
"message": "Cannot delete $ID$",
|
||||||
|
"placeholders": {
|
||||||
|
"id": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "John Smith"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deleteProviderWarningDesc": {
|
||||||
|
"message": "You must unlink all clients before you can delete $ID$",
|
||||||
|
"placeholders": {
|
||||||
|
"id": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "John Smith"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providerDeleted": {
|
||||||
|
"message": "Provider deleted"
|
||||||
|
},
|
||||||
|
"providerDeletedDesc": {
|
||||||
|
"message": "The Provider and all associated data has been deleted."
|
||||||
|
},
|
||||||
|
"deleteProviderRecoverConfirmDesc": {
|
||||||
|
"message": "You have requested to delete this Provider. Use the button below to confirm."
|
||||||
|
},
|
||||||
|
"deleteProviderWarning": {
|
||||||
|
"message": "Deleting your provider is permanent. It cannot be undone."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vau
|
|||||||
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
||||||
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
||||||
|
|
||||||
|
import { DangerZoneComponent } from "../../../../../../apps/web/src/app/auth/settings/account/danger-zone.component";
|
||||||
import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component";
|
import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component";
|
||||||
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
|
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ import { SetupComponent } from "./setup/setup.component";
|
|||||||
ProvidersLayoutComponent,
|
ProvidersLayoutComponent,
|
||||||
PaymentMethodWarningsModule,
|
PaymentMethodWarningsModule,
|
||||||
TaxInfoComponent,
|
TaxInfoComponent,
|
||||||
|
DangerZoneComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AcceptProviderComponent,
|
AcceptProviderComponent,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<app-header></app-header>
|
<app-header></app-header>
|
||||||
|
<bit-container>
|
||||||
<div *ngIf="loading">
|
<div *ngIf="loading">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
@ -49,3 +49,10 @@
|
|||||||
<span>{{ "save" | i18n }}</span>
|
<span>{{ "save" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<app-danger-zone *ngIf="enableDeleteProvider$ | async">
|
||||||
|
<button type="button" bitButton buttonType="danger" (click)="deleteProvider()">
|
||||||
|
{{ "deleteProvider" | i18n }}
|
||||||
|
</button>
|
||||||
|
</app-danger-zone>
|
||||||
|
</bit-container>
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
|
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||||
import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request";
|
import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request";
|
||||||
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
|
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "provider-account",
|
selector: "provider-account",
|
||||||
@ -23,6 +28,11 @@ export class AccountComponent {
|
|||||||
|
|
||||||
private providerId: string;
|
private providerId: string;
|
||||||
|
|
||||||
|
protected enableDeleteProvider$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.EnableDeleteProvider,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
@ -30,6 +40,9 @@ export class AccountComponent {
|
|||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
private providerApiService: ProviderApiServiceAbstraction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -38,7 +51,7 @@ export class AccountComponent {
|
|||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.providerId = params.providerId;
|
this.providerId = params.providerId;
|
||||||
try {
|
try {
|
||||||
this.provider = await this.apiService.getProvider(this.providerId);
|
this.provider = await this.providerApiService.getProvider(this.providerId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(`Handled exception: ${e}`);
|
this.logService.error(`Handled exception: ${e}`);
|
||||||
}
|
}
|
||||||
@ -53,7 +66,7 @@ export class AccountComponent {
|
|||||||
request.businessName = this.provider.businessName;
|
request.businessName = this.provider.businessName;
|
||||||
request.billingEmail = this.provider.billingEmail;
|
request.billingEmail = this.provider.billingEmail;
|
||||||
|
|
||||||
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
|
this.formPromise = this.providerApiService.putProvider(this.providerId, request).then(() => {
|
||||||
return this.syncService.fullSync(true);
|
return this.syncService.fullSync(true);
|
||||||
});
|
});
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
@ -62,4 +75,60 @@ export class AccountComponent {
|
|||||||
this.logService.error(`Handled exception: ${e}`);
|
this.logService.error(`Handled exception: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteProvider() {
|
||||||
|
const providerClients = await this.apiService.getProviderClients(this.providerId);
|
||||||
|
if (providerClients.data != null && providerClients.data.length > 0) {
|
||||||
|
await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "deleteProviderName", placeholders: [this.provider.name] },
|
||||||
|
content: { key: "deleteProviderWarningDesc", placeholders: [this.provider.name] },
|
||||||
|
acceptButtonText: { key: "ok" },
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userVerified = await this.verifyUser();
|
||||||
|
if (!userVerified) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formPromise = this.providerApiService.deleteProvider(this.providerId);
|
||||||
|
try {
|
||||||
|
await this.formPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
this.i18nService.t("providerDeleted"),
|
||||||
|
this.i18nService.t("providerDeletedDesc"),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
this.formPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyUser(): Promise<boolean> {
|
||||||
|
const confirmDescription = "deleteProviderConfirmation";
|
||||||
|
const result = await UserVerificationDialogComponent.open(this.dialogService, {
|
||||||
|
title: "deleteProvider",
|
||||||
|
bodyText: confirmDescription,
|
||||||
|
confirmButtonOptions: {
|
||||||
|
text: "deleteProvider",
|
||||||
|
type: "danger",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle the result of the dialog based on user action and verification success
|
||||||
|
if (result.userAction === "cancel") {
|
||||||
|
// User cancelled the dialog
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User confirmed the dialog so check verification success
|
||||||
|
if (!result.verificationSuccess) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||||
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
|
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
|
||||||
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
|
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
@ -50,10 +50,10 @@ export class SetupComponent implements OnInit {
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private apiService: ApiService,
|
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private providerApiService: ProviderApiServiceAbstraction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -80,7 +80,7 @@ export class SetupComponent implements OnInit {
|
|||||||
|
|
||||||
// Check if provider exists, redirect if it does
|
// Check if provider exists, redirect if it does
|
||||||
try {
|
try {
|
||||||
const provider = await this.apiService.getProvider(this.providerId);
|
const provider = await this.providerApiService.getProvider(this.providerId);
|
||||||
if (provider.name != null) {
|
if (provider.name != null) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
@ -128,7 +128,7 @@ export class SetupComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = await this.apiService.postProviderSetup(this.providerId, request);
|
const provider = await this.providerApiService.postProviderSetup(this.providerId, request);
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
|
||||||
await this.syncService.fullSync(true);
|
await this.syncService.fullSync(true);
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ import {
|
|||||||
InternalPolicyService,
|
InternalPolicyService,
|
||||||
PolicyService as PolicyServiceAbstraction,
|
PolicyService as PolicyServiceAbstraction,
|
||||||
} from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
} from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||||
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||||
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
|
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
|
||||||
@ -47,6 +48,7 @@ import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/comm
|
|||||||
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
|
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
|
||||||
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||||
|
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||||
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
|
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||||
import {
|
import {
|
||||||
@ -1115,6 +1117,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: LoggingErrorHandler,
|
useClass: LoggingErrorHandler,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: ProviderApiServiceAbstraction,
|
||||||
|
useClass: ProviderApiService,
|
||||||
|
deps: [ApiServiceAbstraction],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
function encryptServiceFactory(
|
function encryptServiceFactory(
|
||||||
|
@ -4,8 +4,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re
|
|||||||
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
|
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
|
||||||
import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request";
|
import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request";
|
||||||
import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request";
|
import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request";
|
||||||
import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request";
|
|
||||||
import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request";
|
|
||||||
import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request";
|
import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request";
|
||||||
import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request";
|
import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request";
|
||||||
import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request";
|
import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request";
|
||||||
@ -29,7 +27,6 @@ import {
|
|||||||
ProviderUserResponse,
|
ProviderUserResponse,
|
||||||
ProviderUserUserDetailsResponse,
|
ProviderUserUserDetailsResponse,
|
||||||
} from "../admin-console/models/response/provider/provider-user.response";
|
} from "../admin-console/models/response/provider/provider-user.response";
|
||||||
import { ProviderResponse } from "../admin-console/models/response/provider/provider.response";
|
|
||||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||||
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
|
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
|
||||||
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||||
@ -372,10 +369,6 @@ export abstract class ApiService {
|
|||||||
getPlans: () => Promise<ListResponse<PlanResponse>>;
|
getPlans: () => Promise<ListResponse<PlanResponse>>;
|
||||||
getTaxRates: () => Promise<ListResponse<TaxRateResponse>>;
|
getTaxRates: () => Promise<ListResponse<TaxRateResponse>>;
|
||||||
|
|
||||||
postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>;
|
|
||||||
getProvider: (id: string) => Promise<ProviderResponse>;
|
|
||||||
putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>;
|
|
||||||
|
|
||||||
getProviderUsers: (providerId: string) => Promise<ListResponse<ProviderUserUserDetailsResponse>>;
|
getProviderUsers: (providerId: string) => Promise<ListResponse<ProviderUserUserDetailsResponse>>;
|
||||||
getProviderUser: (providerId: string, id: string) => Promise<ProviderUserResponse>;
|
getProviderUser: (providerId: string, id: string) => Promise<ProviderUserResponse>;
|
||||||
postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise<any>;
|
postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise<any>;
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request";
|
||||||
|
import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request";
|
||||||
|
import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request";
|
||||||
|
import { ProviderResponse } from "../../models/response/provider/provider.response";
|
||||||
|
|
||||||
|
export class ProviderApiServiceAbstraction {
|
||||||
|
postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>;
|
||||||
|
getProvider: (id: string) => Promise<ProviderResponse>;
|
||||||
|
putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>;
|
||||||
|
providerRecoverDeleteToken: (
|
||||||
|
organizationId: string,
|
||||||
|
request: ProviderVerifyRecoverDeleteRequest,
|
||||||
|
) => Promise<any>;
|
||||||
|
deleteProvider: (id: string) => Promise<void>;
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
export class ProviderVerifyRecoverDeleteRequest {
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
constructor(token: string) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import { ApiService } from "../../../abstractions/api.service";
|
||||||
|
import { ProviderApiServiceAbstraction } from "../../abstractions/provider/provider-api.service.abstraction";
|
||||||
|
import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request";
|
||||||
|
import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request";
|
||||||
|
import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request";
|
||||||
|
import { ProviderResponse } from "../../models/response/provider/provider.response";
|
||||||
|
|
||||||
|
export class ProviderApiService implements ProviderApiServiceAbstraction {
|
||||||
|
constructor(private apiService: ApiService) {}
|
||||||
|
async postProviderSetup(id: string, request: ProviderSetupRequest) {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/providers/" + id + "/setup",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return new ProviderResponse(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProvider(id: string) {
|
||||||
|
const r = await this.apiService.send("GET", "/providers/" + id, null, true, true);
|
||||||
|
return new ProviderResponse(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
async putProvider(id: string, request: ProviderUpdateRequest) {
|
||||||
|
const r = await this.apiService.send("PUT", "/providers/" + id, request, true, true);
|
||||||
|
return new ProviderResponse(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
providerRecoverDeleteToken(
|
||||||
|
providerId: string,
|
||||||
|
request: ProviderVerifyRecoverDeleteRequest,
|
||||||
|
): Promise<any> {
|
||||||
|
return this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/providers/" + providerId + "/delete-recover-token",
|
||||||
|
request,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProvider(id: string): Promise<void> {
|
||||||
|
await this.apiService.send("DELETE", "/providers/" + id, null, true, false);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ export enum FeatureFlag {
|
|||||||
EnableConsolidatedBilling = "enable-consolidated-billing",
|
EnableConsolidatedBilling = "enable-consolidated-billing",
|
||||||
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
|
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
|
||||||
UnassignedItemsBanner = "unassigned-items-banner",
|
UnassignedItemsBanner = "unassigned-items-banner",
|
||||||
|
EnableDeleteProvider = "AC-1218-delete-provider",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||||
|
@ -7,8 +7,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re
|
|||||||
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
|
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
|
||||||
import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request";
|
import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request";
|
||||||
import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request";
|
import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request";
|
||||||
import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request";
|
|
||||||
import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request";
|
|
||||||
import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request";
|
import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request";
|
||||||
import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request";
|
import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request";
|
||||||
import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request";
|
import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request";
|
||||||
@ -32,7 +30,6 @@ import {
|
|||||||
ProviderUserResponse,
|
ProviderUserResponse,
|
||||||
ProviderUserUserDetailsResponse,
|
ProviderUserUserDetailsResponse,
|
||||||
} from "../admin-console/models/response/provider/provider-user.response";
|
} from "../admin-console/models/response/provider/provider-user.response";
|
||||||
import { ProviderResponse } from "../admin-console/models/response/provider/provider.response";
|
|
||||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||||
import { TokenService } from "../auth/abstractions/token.service";
|
import { TokenService } from "../auth/abstractions/token.service";
|
||||||
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
|
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
|
||||||
@ -1151,23 +1148,6 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
return this.send("DELETE", "/organizations/connections/" + id, null, true, false);
|
return this.send("DELETE", "/organizations/connections/" + id, null, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider APIs
|
|
||||||
|
|
||||||
async postProviderSetup(id: string, request: ProviderSetupRequest) {
|
|
||||||
const r = await this.send("POST", "/providers/" + id + "/setup", request, true, true);
|
|
||||||
return new ProviderResponse(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProvider(id: string) {
|
|
||||||
const r = await this.send("GET", "/providers/" + id, null, true, true);
|
|
||||||
return new ProviderResponse(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
async putProvider(id: string, request: ProviderUpdateRequest) {
|
|
||||||
const r = await this.send("PUT", "/providers/" + id, request, true, true);
|
|
||||||
return new ProviderResponse(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider User APIs
|
// Provider User APIs
|
||||||
|
|
||||||
async getProviderUsers(
|
async getProviderUsers(
|
||||||
|
Loading…
Reference in New Issue
Block a user