mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 19:53:43 +00:00
Creating a landing page for SM, where user can request access from ad… (#9504)
* Creating a landing page for SM, where user can request access from admins * moving files to better folder, also fixing UI * updating file paths * cleaning up the code * Updating API request to be the new one, and fixing HTML * Adding coowners * Updating OrganizaitonId in the request model to be a Guid * Update apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Suggested changes from Maceij and Thomas * fixing merge issues * fixing issues * Fixing logic to match top bar * updating file name to not start with a capital letter * renaming folder * updating names * Getting around the lint issue * fixing lint issues * Changes requested by Vicky * Maciej suggested changes * Fixing comments * Update apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing-api.service.ts Thomas's suggested improvement Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * ui fixes * New awesome changes, to include the scenario where a Provider user is logged in, and to handle if an admin needs instructions to enable SM for themselves * renaming fuctions and variables --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
import { Guid } from "@bitwarden/common/src/types/guid";
|
||||
|
||||
export class RequestSMAccessRequest {
|
||||
OrganizationId: Guid;
|
||||
EmailContent: string;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<app-header></app-header>
|
||||
|
||||
<bit-container>
|
||||
<form [formGroup]="requestAccessForm" [bitSubmit]="submit">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-9">
|
||||
<p bitTypography="body1">{{ "youNeedApprovalFromYourAdminToTrySecretsManager" | i18n }}</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "addANote" | i18n }}</bit-label>
|
||||
<textarea
|
||||
rows="20"
|
||||
id="request_access_textarea"
|
||||
bitInput
|
||||
formControlName="requestAccessEmailContents"
|
||||
></textarea>
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "organization" | i18n }}</bit-label>
|
||||
<bit-select formControlName="selectedOrganization">
|
||||
<bit-option
|
||||
*ngFor="let org of organizations"
|
||||
[value]="org"
|
||||
[label]="org.name"
|
||||
required
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
<div class="tw-flex tw-gap-x-4 tw-mt-4">
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">
|
||||
{{ "sendRequest" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" [routerLink]="'/sm-landing'">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</bit-container>
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Guid } from "@bitwarden/common/types/guid";
|
||||
import { NoItemsModule, SearchModule, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { OssModule } from "../../oss.module";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { RequestSMAccessRequest } from "../models/requests/request-sm-access.request";
|
||||
|
||||
import { SmLandingApiService } from "./sm-landing-api.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-request-sm-access",
|
||||
standalone: true,
|
||||
templateUrl: "request-sm-access.component.html",
|
||||
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule, OssModule],
|
||||
})
|
||||
export class RequestSMAccessComponent implements OnInit {
|
||||
requestAccessForm = new FormGroup({
|
||||
requestAccessEmailContents: new FormControl(
|
||||
this.i18nService.t("requestAccessSMDefaultEmailContent"),
|
||||
[Validators.required],
|
||||
),
|
||||
selectedOrganization: new FormControl<Organization>(null, [Validators.required]),
|
||||
});
|
||||
organizations: Organization[] = [];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private i18nService: I18nService,
|
||||
private organizationService: OrganizationService,
|
||||
private smLandingApiService: SmLandingApiService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.organizations = (await this.organizationService.getAll())
|
||||
.filter((e) => e.enabled)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (this.organizations === null || this.organizations.length < 1) {
|
||||
await this.navigateToCreateOrganizationPage();
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.requestAccessForm.markAllAsTouched();
|
||||
if (this.requestAccessForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formValue = this.requestAccessForm.value;
|
||||
const request = new RequestSMAccessRequest();
|
||||
request.OrganizationId = formValue.selectedOrganization.id as Guid;
|
||||
request.EmailContent = formValue.requestAccessEmailContents;
|
||||
|
||||
await this.smLandingApiService.requestSMAccessFromAdmins(request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("smAccessRequestEmailSent"),
|
||||
});
|
||||
await this.router.navigate(["/"]);
|
||||
};
|
||||
|
||||
async navigateToCreateOrganizationPage() {
|
||||
await this.router.navigate(["/create-organization"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
|
||||
import { RequestSMAccessRequest } from "../models/requests/request-sm-access.request";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class SmLandingApiService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async requestSMAccessFromAdmins(request: RequestSMAccessRequest): Promise<void> {
|
||||
await this.apiService.send("POST", "/request-access/request-sm-access", request, true, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<app-header></app-header>
|
||||
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-6">
|
||||
<img [src]="imageSrc" class="tw-max-w-full" alt="Bitwarden" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="tw-col-span-6 tw-mx-4">
|
||||
<h1 bitTypography="h1">{{ "bitwardenSecretsManager" | i18n }}</h1>
|
||||
<bit-container *ngIf="this.showSecretsManagerInformation">
|
||||
<p bitTypography="body1">
|
||||
{{ "developmentDevOpsAndITTeamsChooseBWSecret" | i18n }}
|
||||
</p>
|
||||
<ul class="tw-list-outside">
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
<b>{{ "centralizeSecretsManagement" | i18n }}</b>
|
||||
{{ "centralizeSecretsManagementDescription" | i18n }}
|
||||
</li>
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
<b>{{ "preventSecretLeaks" | i18n }}</b> {{ "preventSecretLeaksDescription" | i18n }}
|
||||
</li>
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
<b>{{ "enhanceDeveloperProductivity" | i18n }}</b>
|
||||
{{ "enhanceDeveloperProductivityDescription" | i18n }}
|
||||
</li>
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
<b>{{ "strengthenBusinessSecurity" | i18n }}</b>
|
||||
{{ "strengthenBusinessSecurityDescription" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</bit-container>
|
||||
<bit-container *ngIf="this.showGiveMembersAccessInstructions">
|
||||
<p bitTypography="body1">
|
||||
{{ "giveMembersAccess" | i18n }}
|
||||
</p>
|
||||
<ul class="tw-list-outside">
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
{{ "openYourOrganizations" | i18n }} <b>{{ "members" | i18n }}</b>
|
||||
{{ "viewAndSelectTheMembers" | i18n }}
|
||||
</li>
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
{{ "usingTheMenuSelect" | i18n }} <b>{{ "activateSecretsManager" | i18n }}</b>
|
||||
{{ "toGrantAccessToSelectedMembers" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</bit-container>
|
||||
<button type="button" bitButton buttonType="primary" [routerLink]="tryItNowUrl">
|
||||
{{ "tryItNow" | i18n }}
|
||||
</button>
|
||||
<a bitLink linkType="primary" [href]="learnMoreUrl" target="_blank" class="tw-m-5">
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { NoItemsModule, SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
@Component({
|
||||
selector: "app-sm-landing",
|
||||
standalone: true,
|
||||
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule],
|
||||
templateUrl: "sm-landing.component.html",
|
||||
})
|
||||
export class SMLandingComponent {
|
||||
tryItNowUrl: string;
|
||||
learnMoreUrl: string = "https://bitwarden.com/help/secrets-manager-overview/";
|
||||
imageSrc: string = "../images/sm.webp";
|
||||
showSecretsManagerInformation: boolean = true;
|
||||
showGiveMembersAccessInstructions: boolean = false;
|
||||
|
||||
constructor(private organizationService: OrganizationService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const enabledOrganizations = (await this.organizationService.getAll()).filter((e) => e.enabled);
|
||||
|
||||
if (enabledOrganizations.length > 0) {
|
||||
this.handleEnabledOrganizations(enabledOrganizations);
|
||||
} else {
|
||||
// Person is not part of any orgs they need to be in an organization in order to use SM
|
||||
this.tryItNowUrl = "/create-organization";
|
||||
}
|
||||
}
|
||||
|
||||
private handleEnabledOrganizations(enabledOrganizations: Organization[]) {
|
||||
// People get to this page because SM (Secrets Manager) isn't enabled for them (or the Organization they are a part of)
|
||||
// 1 - SM is enabled for the Organization but not that user
|
||||
//1a - person is Admin+ (Admin or higher) and just needs instructions on how to enable it for themselves
|
||||
//1b - person is beneath admin status and needs to request SM access from Administrators/Owners
|
||||
// 2 - SM is not enabled for the organization yet
|
||||
//2a - person is Owner/Provider - Direct them to the subscription/billing page
|
||||
//2b - person is Admin - Direct them to request access page where an email is sent to owner/admins
|
||||
//2c - person is user - Direct them to request access page where an email is sent to owner/admins
|
||||
|
||||
// We use useSecretsManager because we want to get the first org the person is a part of where SM is enabled but they don't have access enabled yet
|
||||
const adminPlusNeedsInstructionsToEnableSM = enabledOrganizations.find(
|
||||
(o) => o.isAdmin && o.useSecretsManager,
|
||||
);
|
||||
const ownerNeedsToEnableSM = enabledOrganizations.find(
|
||||
(o) => o.isOwner && !o.useSecretsManager,
|
||||
);
|
||||
|
||||
// 1a If Organization has SM Enabled, but this logged in person does not have it enabled, but they are admin+ then give them instructions to enable.
|
||||
if (adminPlusNeedsInstructionsToEnableSM != undefined) {
|
||||
this.showHowToEnableSMForMembers(adminPlusNeedsInstructionsToEnableSM.id);
|
||||
}
|
||||
// 2a Owners can enable SM in the subscription area of Admin Console.
|
||||
else if (ownerNeedsToEnableSM != undefined) {
|
||||
this.tryItNowUrl = `/organizations/${ownerNeedsToEnableSM.id}/billing/subscription`;
|
||||
}
|
||||
// 1b and 2b 2c, they must be lower than an Owner, and they need access, or want their org to have access to SM.
|
||||
else {
|
||||
this.tryItNowUrl = "/request-sm-access";
|
||||
}
|
||||
}
|
||||
|
||||
private showHowToEnableSMForMembers(orgId: string) {
|
||||
this.showGiveMembersAccessInstructions = true;
|
||||
this.showSecretsManagerInformation = false;
|
||||
this.learnMoreUrl =
|
||||
"https://bitwarden.com/help/secrets-manager-quick-start/#give-members-access";
|
||||
this.imageSrc = "../images/sm-give-access.png";
|
||||
this.tryItNowUrl = `/organizations/${orgId}/members`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user