1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-21 11:35:34 +01:00

Auth/PM-5086 - Email Verification - Registration Start + Environment Selector components (#9342)

* PM-5086 - WIP start on registration start component

* PM-5086 - more WIP progress on registration start comp

* PM-5086 - Setup secondary component

* PM-5086 - (1) Validation working (2) States implemented (3) 2nd state for check email mostly completed except for correct icon

* PM-5086 - Registration Start - check email state - update icon to be correct from figma.

* PM-5086 - Refactor self hosted conditional + actually hide the checkbox if it is self hosted.

* PM-5086 - WIP good progress on getting browser & desktop creating account on logic working.

* PM-5086 - Accessibility pass + WIP on region selector

* PM-5086 - Accessibility pass with Danielle

* PM-5086 - Migrate env selector logic to own component.

* PM-5086 - Update AnonLayoutWrapperComp import

* PM-5086 - Remove unncessary focus.

* PM-5086 - WIP first draft of registration env selector; name might change to differentiate it from existing env selector.

* PM-5086 - Rename env selector to be more clear and use registration-env-selector instead.

* PM-5086 - (1) Export registration env selector (2) Change comp name not just file name.

* PM-5086 - Create new registration page stub

* PM-5086 - Fix build issue where select module was missing from new registration env selector.

* PM-5086 - Registration --> registration start.

* PM-5086 - Add missing translation from registration-start-secondary-component to desktop & browser.

* PM-5086 - Add missing translations

* PM-5086 - Registration Env Selector - forms require form groups. duh.

* PM-5086 - Registration Env Selector - working now.

* PM-5086 - Registration Start desktop mostly working with env selector issues.

* PM-5086 - Registration start - get self hosted env dialog to close on close click. Backdrop click doesn't work but escape does still.

* PM-5086 - TODO: figure out if there is a better way to get the dialog to close.

* PM-5086 - Registration start - get goBack working to properly re-show env selector

* PM-5086 - Self Hosted Env Comp - re-emit current env on close so that select based env selectors can reset

* PM-5086 - RegistrationEnvSelector - Refactor init logic to also listen for env updates so that the user's choices on the self hosted settings dialog get communicated to this comp

* PM-5086 - Registration Start Desktop - Don't allow users to close dialog via escape as we need them to either close or save to get the env service to set the env correctly.

* PM-5086 - Browser Registration Start Page stub

* PM-5086 - Registration Start comp - storybook added

* PM-5086 - Remove links to start-registration as we aren't ready to implement that yet.

* PM-5086 - Revert environment comp changes.

* PM-5086 - Delete registration start pages.

* PM-5086 - Test removing PreloadedEnglishI18nModule to see if it fixes test failures

* PM-5086 - Try to resolve issues w/ importing PreloadedEnglishI18nModule into RegistrationStartComponent storybook stories file.

* PM-5086 - Allow translations to be imported for all libs.

* PM-5086 - Remove comment from JSON

* PM-5086 - TODO cleanup

* PM-5086 - Per PR feedback, fix display issues by using correct classes.

* PM-5086 - Fix SVG per PR feedback

* PM-5086 - Remove unnecessary methods

* PM-5086 - RegistrationEnvSelectorComponent - per PR feedback, properly type null in form group
This commit is contained in:
Jared Snider 2024-05-24 15:43:29 -04:00 committed by GitHub
parent 34a766f346
commit 89d7e96b25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 553 additions and 0 deletions

View File

@ -1606,6 +1606,9 @@
"restoredItem": {
"message": "Item restored"
},
"alreadyHaveAccount": {
"message": "Already have an account?"
},
"vaultTimeoutLogOutConfirmation": {
"message": "Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?"
},
@ -2541,6 +2544,27 @@
"ssoIdentifierRequired": {
"message": "Organization SSO identifier is required."
},
"creatingAccountOn": {
"message": "Creating account on"
},
"checkYourEmail": {
"message": "Check your email"
},
"followTheLinkInTheEmailSentTo": {
"message": "Follow the link in the email sent to"
},
"andContinueCreatingYourAccount": {
"message": "and continue creating your account."
},
"noEmail": {
"message": "No email?"
},
"goBack": {
"message": "Go back"
},
"toEditYourEmailAddress": {
"message": "to edit your email address."
},
"eu": {
"message": "EU",
"description": "European Union"

View File

@ -2050,6 +2050,9 @@
"switchAccount": {
"message": "Switch account"
},
"alreadyHaveAccount": {
"message": "Already have an account?"
},
"options": {
"message": "Options"
},
@ -2390,6 +2393,27 @@
"logInRequested": {
"message": "Log in requested"
},
"creatingAccountOn": {
"message": "Creating account on"
},
"checkYourEmail": {
"message": "Check your email"
},
"followTheLinkInTheEmailSentTo": {
"message": "Follow the link in the email sent to"
},
"andContinueCreatingYourAccount": {
"message": "and continue creating your account."
},
"noEmail": {
"message": "No email?"
},
"goBack": {
"message": "Go back"
},
"toEditYourEmailAddress": {
"message": "to edit your email address."
},
"exposedMasterPassword": {
"message": "Exposed Master Password"
},

View File

@ -3244,6 +3244,27 @@
"device": {
"message": "Device"
},
"creatingAccountOn": {
"message": "Creating account on"
},
"checkYourEmail": {
"message": "Check your email"
},
"followTheLinkInTheEmailSentTo": {
"message": "Follow the link in the email sent to"
},
"andContinueCreatingYourAccount": {
"message": "and continue creating your account."
},
"noEmail": {
"message": "No email?"
},
"goBack": {
"message": "Go back"
},
"toEditYourEmailAddress": {
"message": "to edit your email address."
},
"view": {
"message": "View"
},

View File

@ -0,0 +1,12 @@
import { svgIcon } from "@bitwarden/components";
export const RegistrationCheckEmailIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" width="144" height="113" fill="none">
<g fill="#175DDC" clip-path="url(#a)">
<path class="tw-fill-primary-600" fill-rule="evenodd" d="M39.19 26.64c0-.765.62-1.384 1.383-1.384h18.785a1.383 1.383 0 0 1 0 2.766H40.573c-.764 0-1.383-.619-1.383-1.383ZM39.19 37.819c0-.764.62-1.383 1.383-1.383H58.81a1.383 1.383 0 0 1 0 2.766H40.573c-.764 0-1.383-.62-1.383-1.383ZM39.19 48.998c0-.764.62-1.383 1.383-1.383H61.82a1.383 1.383 0 0 1 0 2.766H40.573c-.764 0-1.383-.62-1.383-1.383Z" clip-rule="evenodd"/>
<path class="tw-fill-primary-600" d="M94.576 60.504c0 .382.31.692.691.692 14.892 0 26.977-11.935 26.977-26.675a.692.692 0 0 0-1.383 0c0 13.96-11.451 25.292-25.594 25.292a.691.691 0 0 0-.691.691ZM68.982 35.213c.382 0 .692-.31.692-.692 0-13.96 11.45-25.291 25.593-25.291a.691.691 0 1 0 0-1.383c-14.89 0-26.976 11.935-26.976 26.674 0 .382.31.692.691.692Z"/>
<path class="tw-fill-primary-600" fill-rule="evenodd" d="M95.387 2.926c-6.295 0-12.16 1.84-17.086 5.013l-1.136-.877a9.68 9.68 0 0 0-11.705-.096l-8.172 6.098a1.33 1.33 0 0 0-.064.05H33.393a4.149 4.149 0 0 0-4.149 4.15v16.753c-.07.035-.14.078-.205.127l-8.464 6.315a9.68 9.68 0 0 0-3.89 7.759v51.696c0 5.346 4.333 9.68 9.68 9.68h90.733c5.346 0 9.68-4.334 9.68-9.68v-26.48l8.099 8.098a4.84 4.84 0 0 0 6.845 0l.721-.721a4.84 4.84 0 0 0 0-6.845l-15.665-15.665V44.056c0-.587-.366-1.089-.882-1.29a31.666 31.666 0 0 0 1.086-8.245c0-17.45-14.146-31.595-31.595-31.595Zm28.625 52.61v-7.624a31.463 31.463 0 0 1-2.803 4.82l2.803 2.803Zm-4.51-.6a31.84 31.84 0 0 1-3.651 3.659l20.981 20.982c.81.81 2.124.81 2.934 0l.721-.722a2.073 2.073 0 0 0 0-2.933l-20.985-20.986Zm-5.851 5.37a31.434 31.434 0 0 1-16.653 5.77l-.023.014-9.463 5.427a15.205 15.205 0 0 1 2.167 1.666l33.149 30.601a6.876 6.876 0 0 0 1.184-3.87V70.668l-10.361-10.362ZM29.244 37.442l-7.014 5.234a6.914 6.914 0 0 0-2.588 3.925c.062.025.123.056.183.091l9.42 5.626V37.442Zm2.766 16.527 8.172 4.881c.124-.036.255-.056.39-.056h27.813a1.383 1.383 0 0 1 0 2.766H44.72l13.656 8.156a15.208 15.208 0 0 1 4.116-.567H79.36c1.801 0 3.571.32 5.233.928.095-.103.206-.193.334-.267l6.805-3.902C76.004 64.097 63.791 50.735 63.791 34.52a31.454 31.454 0 0 1 6.082-18.64h-36.48c-.764 0-1.383.619-1.383 1.383v36.705Zm40.135-40.851a31.78 31.78 0 0 1 3.8-3.504l-.47-.363a6.914 6.914 0 0 0-8.36-.068l-5.27 3.932h10.212c.03 0 .059 0 .088.003ZM66.557 34.52c0-15.922 12.907-28.83 28.83-28.83 15.922 0 28.829 12.908 28.829 28.83 0 15.922-12.907 28.83-28.83 28.83-15.922 0-28.83-12.908-28.83-28.83ZM19.45 49.691v50.223c0 1.418.428 2.738 1.16 3.835l31.298-30.315a15.21 15.21 0 0 1 3.266-2.41L19.45 49.69Zm3.124 56.007a6.888 6.888 0 0 0 3.79 1.13h90.734a6.879 6.879 0 0 0 3.752-1.106L87.803 75.216a12.446 12.446 0 0 0-8.442-3.301H62.49a12.446 12.446 0 0 0-8.658 3.506l-31.259 30.277Z" clip-rule="evenodd"/></g><defs><clipPath id="a">
<path class="tw-fill-primary-600" fill="#fff" d="M.09 0h143.82v112.705H.09z"/>
</clipPath>
</defs>
</svg>`;

View File

@ -14,3 +14,8 @@ export * from "./password-callout/password-callout.component";
export * from "./user-verification/user-verification-dialog.component";
export * from "./user-verification/user-verification-dialog.types";
export * from "./user-verification/user-verification-form-input.component";
// registration
export * from "./registration/registration-start/registration-start.component";
export * from "./registration/registration-start/registration-start-secondary.component";
export * from "./registration/registration-env-selector/registration-env-selector.component";

View File

@ -0,0 +1,16 @@
<form [formGroup]="formGroup">
<bit-form-field>
<bit-label>{{ "creatingAccountOn" | i18n }}</bit-label>
<bit-select formControlName="selectedRegion">
<bit-option
*ngFor="let regionConfig of availableRegionConfigs"
[value]="regionConfig"
[label]="regionConfig.domain"
></bit-option>
<bit-option
[value]="ServerEnvironmentType.SelfHosted"
[label]="'selfHostedServer' | i18n"
></bit-option>
</bit-select>
</bit-form-field>
</form>

View File

@ -0,0 +1,101 @@
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
import { EMPTY, Subject, from, map, of, switchMap, takeUntil, tap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
Environment,
EnvironmentService,
Region,
RegionConfig,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { FormFieldModule, SelectModule } from "@bitwarden/components";
@Component({
standalone: true,
selector: "auth-registration-env-selector",
templateUrl: "registration-env-selector.component.html",
imports: [CommonModule, JslibModule, ReactiveFormsModule, FormFieldModule, SelectModule],
})
export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
@Output() onOpenSelfHostedSettings = new EventEmitter();
ServerEnvironmentType = Region;
formGroup = this.formBuilder.group({
selectedRegion: [null as RegionConfig | Region.SelfHosted | null, Validators.required],
});
get selectedRegion(): FormControl {
return this.formGroup.get("selectedRegion") as FormControl;
}
availableRegionConfigs: RegionConfig[] = this.environmentService.availableRegions();
private destroy$ = new Subject<void>();
constructor(
private formBuilder: FormBuilder,
private environmentService: EnvironmentService,
) {}
async ngOnInit() {
await this.initSelectedRegionAndListenForEnvChanges();
this.listenForSelectedRegionChanges();
}
private async initSelectedRegionAndListenForEnvChanges() {
this.environmentService.environment$
.pipe(
map((env: Environment) => {
const region: Region = env.getRegion();
const regionConfig: RegionConfig = this.availableRegionConfigs.find(
(availableRegionConfig) => availableRegionConfig.key === region,
);
if (regionConfig === undefined) {
// Self hosted does not have a region config.
return Region.SelfHosted;
}
return regionConfig;
}),
tap((selectedRegionInitialValue: RegionConfig | Region.SelfHosted) => {
// This inits the form control with the selected region, but
// it also sets the value to self hosted if the self hosted settings are saved successfully
// in the client specific implementation managed by the parent component.
// It also resets the value to the previously selected region if the self hosted
// settings are closed without saving. We don't emit the event to avoid a loop.
this.selectedRegion.setValue(selectedRegionInitialValue, { emitEvent: false });
}),
takeUntil(this.destroy$),
)
.subscribe();
}
private listenForSelectedRegionChanges() {
this.selectedRegion.valueChanges
.pipe(
switchMap((selectedRegionConfig: RegionConfig | Region.SelfHosted | null) => {
if (selectedRegionConfig === null) {
return of(null);
}
if (selectedRegionConfig === Region.SelfHosted) {
this.onOpenSelfHostedSettings.emit();
return EMPTY;
}
return from(this.environmentService.setEnvironment(selectedRegionConfig.key));
}),
takeUntil(this.destroy$),
)
.subscribe();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -0,0 +1,3 @@
<span
>{{ "alreadyHaveAccount" | i18n }} <a routerLink="/login">{{ "logIn" | i18n }}</a></span
>

View File

@ -0,0 +1,15 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@Component({
standalone: true,
selector: "auth-registration-start-secondary",
templateUrl: "./registration-start-secondary.component.html",
imports: [CommonModule, JslibModule, RouterModule],
})
export class RegistrationStartSecondaryComponent {
constructor() {}
}

View File

@ -0,0 +1,83 @@
<ng-container *ngIf="state === RegistrationStartState.USER_DATA_ENTRY">
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-form-field>
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
<input
id="register-start_form_input_email"
bitInput
type="email"
formControlName="email"
[attr.readonly]="emailReadonly ? true : null"
appAutofocus
/>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "name" | i18n }}</bit-label>
<input id="register-start_form_input_name" bitInput type="text" formControlName="name" />
</bit-form-field>
<bit-form-control *ngIf="!isSelfHost">
<input
id="register-start-form-input-accept-policies"
type="checkbox"
bitCheckbox
formControlName="acceptPolicies"
/>
<bit-label for="register-start-form-input-accept-policies">
{{ "acceptPolicies" | i18n }}
<a
bitLink
linkType="primary"
href="https://bitwarden.com/terms/"
target="_blank"
rel="noreferrer"
>{{ "termsOfService" | i18n }}</a
>,
<a
bitLink
linkType="primary"
href="https://bitwarden.com/privacy/"
target="_blank"
rel="noreferrer"
>{{ "privacyPolicy" | i18n }}</a
>
</bit-label>
</bit-form-control>
<button [block]="true" type="submit" buttonType="primary" bitButton bitFormButton>
{{ "continue" | i18n }}
</button>
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary></form
></ng-container>
<ng-container *ngIf="state === RegistrationStartState.CHECK_EMAIL">
<div class="tw-flex tw-flex-col tw-items-center tw-justify-center">
<bit-icon [icon]="Icons.RegistrationCheckEmailIcon" class="tw-mb-6"></bit-icon>
<h2
bitTypography="h2"
id="check_your_email_heading"
class="tw-font-bold tw-mb-3 tw-text-main"
tabindex="0"
appAutofocus
aria-describedby="follow_the_link_body"
>
{{ "checkYourEmail" | i18n }}
</h2>
<p bitTypography="body1" class="tw-text-center tw-mb-3 tw-text-main" id="follow_the_link_body">
{{ "followTheLinkInTheEmailSentTo" | i18n }}
<span class="tw-font-bold">{{ email.value }}</span>
{{ "andContinueCreatingYourAccount" | i18n }}
</p>
<p bitTypography="helper" class="tw-text-center tw-text-main">
{{ "noEmail" | i18n }}
<a bitLink linkType="primary" class="tw-cursor-pointer" tabindex="0" (click)="goBack()">
{{ "goBack" | i18n }}
</a>
{{ "toEditYourEmailAddress" | i18n }}
</p>
</div>
</ng-container>

View File

@ -0,0 +1,146 @@
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
import {
AbstractControl,
FormBuilder,
FormControl,
ReactiveFormsModule,
ValidatorFn,
Validators,
} from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
AsyncActionsModule,
ButtonModule,
CheckboxModule,
FormFieldModule,
IconModule,
LinkModule,
} from "@bitwarden/components";
import { RegistrationCheckEmailIcon } from "../../icons/registration-check-email.icon";
export enum RegistrationStartState {
USER_DATA_ENTRY = "UserDataEntry",
CHECK_EMAIL = "CheckEmail",
}
@Component({
standalone: true,
selector: "auth-registration-start",
templateUrl: "./registration-start.component.html",
imports: [
CommonModule,
ReactiveFormsModule,
JslibModule,
FormFieldModule,
AsyncActionsModule,
CheckboxModule,
ButtonModule,
LinkModule,
IconModule,
],
})
export class RegistrationStartComponent implements OnInit, OnDestroy {
@Output() registrationStartStateChange = new EventEmitter<RegistrationStartState>();
state: RegistrationStartState = RegistrationStartState.USER_DATA_ENTRY;
RegistrationStartState = RegistrationStartState;
readonly Icons = { RegistrationCheckEmailIcon };
isSelfHost = false;
formGroup = this.formBuilder.group({
email: ["", [Validators.required, Validators.email]],
name: [""],
acceptPolicies: [false, [this.acceptPoliciesValidator()]],
selectedRegion: [null],
});
get email(): FormControl {
return this.formGroup.get("email") as FormControl;
}
get name(): FormControl {
return this.formGroup.get("name") as FormControl;
}
get acceptPolicies(): FormControl {
return this.formGroup.get("acceptPolicies") as FormControl;
}
emailReadonly: boolean = false;
showErrorSummary = false;
private destroy$ = new Subject<void>();
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private platformUtilsService: PlatformUtilsService,
) {
this.isSelfHost = platformUtilsService.isSelfHost();
}
async ngOnInit() {
// Emit the initial state
this.registrationStartStateChange.emit(this.state);
this.listenForQueryParamChanges();
}
private listenForQueryParamChanges() {
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => {
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
this.email?.setValue(qParams.email);
this.emailReadonly = qParams.emailReadonly === "true";
}
});
}
submit = async () => {
const valid = this.validateForm();
if (!valid) {
return;
}
// TODO: Implement registration logic
this.state = RegistrationStartState.CHECK_EMAIL;
this.registrationStartStateChange.emit(this.state);
};
private validateForm(): boolean {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
this.showErrorSummary = true;
}
return this.formGroup.valid;
}
goBack() {
this.state = RegistrationStartState.USER_DATA_ENTRY;
this.registrationStartStateChange.emit(this.state);
}
private acceptPoliciesValidator(): ValidatorFn {
return (control: AbstractControl) => {
const ctrlValue = control.value;
return !ctrlValue && !this.isSelfHost ? { required: true } : null;
};
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -0,0 +1,28 @@
import { Meta, Story, Controls } from "@storybook/addon-docs";
import * as stories from "./registration-start.stories";
<Meta of={stories} />
# RegistrationStart Component
The Auth-owned RegistrationStartComponent is to be used for the first step in the new email
verification stagegated registration process. It collects the user's email address (required) and
optionally their name. On cloud environments, it requires acceptance of the terms of service and the
privacy policy; the checkbox is hidden on self hosted environments.
### Cloud Example
<Story of={stories.CloudExample} />
### Self Hosted Example
<Story of={stories.SelfHostExample} />
### Query Param Example
The component accepts two query parameters: `email` and `emailReadonly`. If an email is provided, it
will be pre-filled in the email input field. If `emailReadonly` is set to `true`, the email input
field will be set to readonly. `emailReadonly` is primarily for the organization invite flow.
<Story of={stories.QueryParamsExample} />

View File

@ -0,0 +1,74 @@
import { importProvidersFrom } from "@angular/core";
import { ActivatedRoute, Params } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { of } from "rxjs";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { PreloadedEnglishI18nModule } from "../../../../../../apps/web/src/app/core/tests";
import { RegistrationStartComponent } from "./registration-start.component";
export default {
title: "Auth/Registration/Registration Start",
component: RegistrationStartComponent,
} as Meta;
const decorators = (options: { isSelfHost: boolean; queryParams: Params }) => {
return [
moduleMetadata({
imports: [RouterTestingModule],
providers: [
{
provide: ActivatedRoute,
useValue: { queryParams: of(options.queryParams) },
},
{
provide: PlatformUtilsService,
useValue: {
isSelfHost: () => options.isSelfHost,
} as Partial<PlatformUtilsService>,
},
],
}),
applicationConfig({
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
}),
];
};
type Story = StoryObj<RegistrationStartComponent>;
export const CloudExample: Story = {
render: (args) => ({
props: args,
template: `
<auth-registration-start></auth-registration-start>
`,
}),
decorators: decorators({ isSelfHost: false, queryParams: {} }),
};
export const SelfHostExample: Story = {
render: (args) => ({
props: args,
template: `
<auth-registration-start></auth-registration-start>
`,
}),
decorators: decorators({ isSelfHost: true, queryParams: {} }),
};
export const QueryParamsExample: Story = {
render: (args) => ({
props: args,
template: `
<auth-registration-start></auth-registration-start>
`,
}),
decorators: decorators({
isSelfHost: false,
queryParams: { email: "jaredWasHere@bitwarden.com", emailReadonly: "true" },
}),
};

View File

@ -1,6 +1,7 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"resolveJsonModule": true,
"paths": {
"@bitwarden/admin-console": ["../admin-console/src"],
"@bitwarden/angular/*": ["../angular/src/*"],