1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-01 13:13:36 +01:00

Merge branch 'feature/sm-billing' into AC-1418-add-secrets-manager-manage-subscription-component

This commit is contained in:
Thomas Rittson 2023-06-29 10:55:39 +10:00 committed by GitHub
commit 1c39938cda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 1548 additions and 394 deletions

9
.github/CODEOWNERS vendored
View File

@ -4,7 +4,7 @@
# The following owners will be the default owners for everything in the repo. # The following owners will be the default owners for everything in the repo.
# Unless a later match takes precedence # Unless a later match takes precedence
@bitwarden/team-leads * @bitwarden/team-leads-eng
## Secrets Manager team files ## ## Secrets Manager team files ##
bitwarden_license/bit-web/src/app/secrets-manager @bitwarden/team-secrets-manager-dev bitwarden_license/bit-web/src/app/secrets-manager @bitwarden/team-secrets-manager-dev
@ -76,3 +76,10 @@ libs/components @bitwarden/team-platform-dev
## Desktop native module ## ## Desktop native module ##
apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native @bitwarden/team-platform-dev
## Multiple file owners ##
/apps/web/config
/apps/web/package.json
## DevOps team files ##
/.github/workflows @bitwarden/dept-devops

View File

@ -4,7 +4,6 @@
./apps/browser/src/safari/desktop/Base.lproj ./apps/browser/src/safari/desktop/Base.lproj
./apps/browser/src/services/vaultTimeout ./apps/browser/src/services/vaultTimeout
./apps/browser/store/windows/Assets ./apps/browser/store/windows/Assets
./libs/common/src/abstractions/userVerification
./libs/common/src/abstractions/vaultTimeout ./libs/common/src/abstractions/vaultTimeout
./libs/common/src/services/vaultTimeout ./libs/common/src/services/vaultTimeout
./bitwarden_license/README.md ./bitwarden_license/README.md
@ -26,8 +25,6 @@
./libs/common/src/misc/linkedFieldOption.decorator.ts ./libs/common/src/misc/linkedFieldOption.decorator.ts
./libs/common/src/misc/serviceUtils.ts ./libs/common/src/misc/serviceUtils.ts
./libs/common/src/misc/serviceUtils.spec.ts ./libs/common/src/misc/serviceUtils.spec.ts
./libs/common/src/abstractions/userVerification/userVerification.service.abstraction.ts
./libs/common/src/abstractions/userVerification/userVerification-api.service.abstraction.ts
./libs/common/src/abstractions/vaultTimeout/vaultTimeoutSettings.service.ts ./libs/common/src/abstractions/vaultTimeout/vaultTimeoutSettings.service.ts
./libs/common/src/abstractions/vaultTimeout/vaultTimeout.service.ts ./libs/common/src/abstractions/vaultTimeout/vaultTimeout.service.ts
./libs/common/src/abstractions/anonymousHub.service.ts ./libs/common/src/abstractions/anonymousHub.service.ts

View File

@ -84,6 +84,8 @@ jobs:
npm_command: "build:bit:ee" npm_command: "build:bit:ee"
- name: "cloud-euprd" - name: "cloud-euprd"
npm_command: "build:bit:euprd" npm_command: "build:bit:euprd"
- name: "cloud-euqa"
npm_command: "build:bit:euqa"
steps: steps:
- name: Checkout repo - name: Checkout repo

60
.github/workflows/deploy-eu-qa-web.yml vendored Normal file
View File

@ -0,0 +1,60 @@
---
name: Deploy Web to EU-QA Cloud
on:
workflow_dispatch:
inputs:
tag:
description: "Branch name to deploy (examples: 'master', 'feature/sm')"
required: true
type: string
default: master
jobs:
azure-deploy:
name: Deploy to Azure
runs-on: ubuntu-22.04
env:
_WEB_ARTIFACT: "web-*-cloud-euqa.zip"
steps:
- name: Login to Azure - EU Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
with:
creds: ${{ secrets.AZURE_KV_EU_QA_SERVICE_PRINCIPAL }}
- name: Retrieve Storage Account connection string
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@37ffa14164a7308bc273829edfe75c97cd562375
with:
keyvault: webvaulteu-westeurope-qa
secrets: "sa-bitwarden-web-vault-dev-key-temp"
- name: Download latest cloud asset
uses: bitwarden/gh-actions/download-artifacts@37ffa14164a7308bc273829edfe75c97cd562375
with:
workflow: build-web.yml
path: apps/web
workflow_conclusion: success
branch: ${{ github.event.inputs.tag }}
artifacts: ${{ env._WEB_ARTIFACT }}
- name: Unzip build asset
working-directory: apps/web
run: unzip ${{ env._WEB_ARTIFACT }}
- name: Empty container in Storage Account
run: |
az storage blob delete-batch \
--source '$web' \
--pattern '*' \
--connection-string "${{ steps.retrieve-secrets.outputs.sa-bitwarden-web-vault-dev-key-temp }}"
- name: Deploy to Azure Storage Account
working-directory: apps/web
run: |
az storage blob upload-batch \
--source "./build" \
--destination '$web' \
--connection-string "${{ steps.retrieve-secrets.outputs.sa-bitwarden-web-vault-dev-key-temp }}" \
--overwrite \
--no-progress

View File

@ -12,8 +12,6 @@ on:
type: choice type: choice
options: options:
- QA - QA
- POC2
- eudevtest
jobs: jobs:

View File

@ -101,6 +101,7 @@ const preview: Preview = {
}, },
options: { options: {
storySort: { storySort: {
method: "alphabetical",
order: ["Documentation", ["Introduction", "Colors", "Icons"], "Component Library"], order: ["Documentation", ["Introduction", "Colors", "Icons"], "Component Library"],
}, },
}, },

View File

@ -952,7 +952,7 @@
"message": "Server URL" "message": "Server URL"
}, },
"apiUrl": { "apiUrl": {
"message": "API Server URL" "message": "API server URL"
}, },
"webVaultUrl": { "webVaultUrl": {
"message": "Web vault server URL" "message": "Web vault server URL"

View File

@ -7,8 +7,6 @@ import { NotificationsService as NotificationsServiceAbstraction } from "@bitwar
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/abstractions/userVerification/userVerification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { InternalOrganizationService as InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { InternalOrganizationService as InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@ -21,6 +19,8 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service";

View File

@ -15,7 +15,6 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notificatio
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@ -31,6 +30,7 @@ import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-con
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { LoginService } from "@bitwarden/common/auth/services/login.service"; import { LoginService } from "@bitwarden/common/auth/services/login.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";

View File

@ -5,8 +5,8 @@ import { Router } from "@angular/router";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component"; import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@ -6,8 +6,8 @@ import { UntypedFormBuilder } from "@angular/forms";
import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component"; import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";

View File

@ -1,11 +0,0 @@
{
"urls": {
"icons": "https://icons.eudevtest.bitwarden.pw",
"notifications": "https://notifications.eudevtest.bitwarden.pw",
"scim": "https://scim.eudevtest.bitwarden.pw"
},
"flags": {
"secretsManager": true,
"showPasswordless": true
}
}

View File

@ -1,6 +1,6 @@
{ {
"urls": { "urls": {
"icons": "https://icons.bitwarden.net", "icons": "https://icons.bitwarden.eu",
"notifications": "https://notifications.bitwarden.eu", "notifications": "https://notifications.bitwarden.eu",
"scim": "https://scim.bitwarden.eu" "scim": "https://scim.bitwarden.eu"
}, },

11
apps/web/config/euqa.json Normal file
View File

@ -0,0 +1,11 @@
{
"urls": {
"icons": "https://icons.euqa.bitwarden.pw",
"notifications": "https://notifications.euqa.bitwarden.pw",
"scim": "https://scim.euqa.bitwarden.pw"
},
"flags": {
"secretsManager": true,
"showPasswordless": true
}
}

View File

@ -11,7 +11,7 @@
"build:bit:dev:watch": "cross-env ENV=development npm run build:bit:watch", "build:bit:dev:watch": "cross-env ENV=development npm run build:bit:watch",
"build:bit:qa": "cross-env NODE_ENV=production ENV=qa npm run build:bit", "build:bit:qa": "cross-env NODE_ENV=production ENV=qa npm run build:bit",
"build:bit:euprd": "cross-env NODE_ENV=production ENV=euprd npm run build:bit", "build:bit:euprd": "cross-env NODE_ENV=production ENV=euprd npm run build:bit",
"build:bit:eudevtest": "cross-env NODE_ENV=production ENV=eudevtest npm run build:bit", "build:bit:euqa": "cross-env NODE_ENV=production ENV=euqa npm run build:bit",
"build:bit:cloud": "cross-env NODE_ENV=production ENV=cloud npm run build:bit", "build:bit:cloud": "cross-env NODE_ENV=production ENV=cloud npm run build:bit",
"build:oss:selfhost:watch": "cross-env ENV=selfhosted npm run build:oss:watch", "build:oss:selfhost:watch": "cross-env ENV=selfhosted npm run build:oss:watch",
"build:bit:selfhost:watch": "cross-env ENV=selfhosted npm run build:bit:watch", "build:bit:selfhost:watch": "cross-env ENV=selfhosted npm run build:bit:watch",

View File

@ -4,10 +4,10 @@ import { FormBuilder, FormControl, Validators } from "@angular/forms";
import { combineLatest, Subject, takeUntil } from "rxjs"; import { combineLatest, Subject, takeUntil } from "rxjs";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
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";
@ -17,7 +17,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { UserVerificationModule } from "../../../../shared/components/user-verification"; import { UserVerificationModule } from "../../../../auth/shared/components/user-verification";
import { SharedModule } from "../../../../shared/shared.module"; import { SharedModule } from "../../../../shared/shared.module";
class CountBasedLocalizationKey { class CountBasedLocalizationKey {

View File

@ -3,10 +3,9 @@ import { UntypedFormBuilder } from "@angular/forms";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@ -35,7 +34,6 @@ export class OrganizationExportComponent extends ExportComponent {
userVerificationService: UserVerificationService, userVerificationService: UserVerificationService,
formBuilder: UntypedFormBuilder, formBuilder: UntypedFormBuilder,
fileDownloadService: FileDownloadService, fileDownloadService: FileDownloadService,
modalService: ModalService,
dialogService: DialogServiceAbstraction dialogService: DialogServiceAbstraction
) { ) {
super( super(
@ -49,7 +47,6 @@ export class OrganizationExportComponent extends ExportComponent {
userVerificationService, userVerificationService,
formBuilder, formBuilder,
fileDownloadService, fileDownloadService,
modalService,
dialogService dialogService
); );
} }

View File

@ -4,9 +4,9 @@ import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalConfig } from "@bitwarden/angular/services/modal.service"; import { ModalConfig } from "@bitwarden/angular/services/modal.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests"; import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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";

View File

@ -1,8 +1,8 @@
import { ScrollingModule } from "@angular/cdk/scrolling"; import { ScrollingModule } from "@angular/cdk/scrolling";
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { UserVerificationModule } from "../../../auth/shared/components/user-verification";
import { LooseComponentsModule, SharedModule } from "../../../shared"; import { LooseComponentsModule, SharedModule } from "../../../shared";
import { UserVerificationModule } from "../../../shared/components/user-verification";
import { EnrollMasterPasswordReset } from "./enroll-master-password-reset.component"; import { EnrollMasterPasswordReset } from "./enroll-master-password-reset.component";

View File

@ -1,7 +1,7 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";

View File

@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request"; import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request";
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response"; import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response";

View File

@ -2,7 +2,7 @@ import { Directive, EventEmitter, Output } from "@angular/core";
import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";

View File

@ -2,7 +2,7 @@ import { Component } from "@angular/core";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/auth/models/request/update-two-factor-duo.request"; import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/auth/models/request/update-two-factor-duo.request";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response"; import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";

View File

@ -2,7 +2,7 @@ import { Component } from "@angular/core";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/update-two-factor-email.request"; import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/update-two-factor-email.request";

View File

@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";

View File

@ -2,7 +2,7 @@ import { Component, NgZone } from "@angular/core";
import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn-delete.request"; import { UpdateTwoFactorWebAuthnDeleteRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn-delete.request";

View File

@ -2,7 +2,7 @@ import { Component } from "@angular/core";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { UpdateTwoFactorYubioOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubio-otp.request"; import { UpdateTwoFactorYubioOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubio-otp.request";
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response"; import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response";

View File

@ -1,3 +1,3 @@
export * from "./user-verification.module"; export * from "./user-verification.module";
export * from "./user-verification-prompt.component";
export * from "./user-verification.component"; export * from "./user-verification.component";
export * from "./user-verification-prompt.component";

View File

@ -0,0 +1,20 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog>
<span bitDialogTitle>{{ modalTitle | i18n }}</span>
<ng-container bitDialogContent>
<p bitTypography="body1">{{ confirmDescription | i18n }}</p>
<app-user-verification
[(invalidSecret)]="invalidSecret"
formControlName="secret"
></app-user-verification>
</ng-container>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ confirmButtonText | i18n }}
</button>
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@ -0,0 +1,60 @@
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { UserVerificationPromptComponent as BaseUserVerificationPrompt } from "@bitwarden/angular/auth/components/user-verification-prompt.component";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ModalConfig } from "@bitwarden/angular/services/modal.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
export interface UserVerificationPromptParams {
confirmDescription: string;
confirmButtonText: string;
modalTitle: string;
}
@Component({
templateUrl: "user-verification-prompt.component.html",
})
export class UserVerificationPromptComponent extends BaseUserVerificationPrompt {
constructor(
@Inject(DIALOG_DATA) data: UserVerificationPromptParams,
private dialogRef: DialogRef<boolean>,
userVerificationService: UserVerificationService,
formBuilder: FormBuilder,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService
) {
// TODO: Remove when BaseUserVerificationPrompt has support for CL
const modalConfig: ModalConfig = { data };
super(
null,
modalConfig,
userVerificationService,
formBuilder,
platformUtilsService,
i18nService
);
}
override close(success: boolean) {
this.dialogRef.close(success);
}
}
/**
* Strongly typed helper to open a UserVerificationPrompt
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export const openUserVerificationPrompt = (
dialogService: DialogServiceAbstraction,
config: DialogConfig<UserVerificationPromptParams>
) => {
return dialogService.open<boolean, UserVerificationPromptParams>(
UserVerificationPromptComponent,
config
);
};

View File

@ -0,0 +1,41 @@
<ng-container *ngIf="!usesKeyConnector">
<bit-form-field disableMargin>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
bitInput
id="masterPassword"
type="password"
name="MasterPasswordHash"
[formControl]="secret"
appAutofocus
appInputVerbatim
/>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<bit-hint>{{ "confirmIdentity" | i18n }}</bit-hint>
</bit-form-field>
</ng-container>
<ng-container *ngIf="usesKeyConnector">
<div class="tw-mb-6">
<label class="tw-block">{{ "sendVerificationCode" | i18n }}</label>
<button type="button" bitButton buttonType="secondary" [bitAction]="requestOTP" appAutofocus>
{{ "sendCode" | i18n }}
</button>
<span class="tw-ml-2 tw-text-success" role="alert" @sent *ngIf="sentCode">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "codeSent" | i18n }}
</span>
</div>
<bit-form-field disableMargin>
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
<input
bitInput
type="text"
id="verificationCode"
name="verificationCode"
[formControl]="secret"
appInputVerbatim
/>
<bit-hint>{{ "confirmIdentity" | i18n }}</bit-hint>
</bit-form-field>
</ng-container>

View File

@ -1,12 +1,13 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { SharedModule } from "../../shared.module"; import { SharedModule } from "../../../../shared/shared.module";
import { UserVerificationPromptComponent } from "./user-verification-prompt.component"; import { UserVerificationPromptComponent } from "./user-verification-prompt.component";
import { UserVerificationComponent } from "./user-verification.component"; import { UserVerificationComponent } from "./user-verification.component";
@NgModule({ @NgModule({
imports: [SharedModule], imports: [SharedModule, FormsModule, ReactiveFormsModule],
declarations: [UserVerificationComponent, UserVerificationPromptComponent], declarations: [UserVerificationComponent, UserVerificationPromptComponent],
exports: [UserVerificationComponent, UserVerificationPromptComponent], exports: [UserVerificationComponent, UserVerificationPromptComponent],
}) })

View File

@ -4,8 +4,8 @@ import { Router } from "@angular/router";
import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component"; import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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";

View File

@ -2,10 +2,10 @@ import { Component } from "@angular/core";
import { ModalConfig } from "@bitwarden/angular/services/modal.service"; import { ModalConfig } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
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 { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums";
import { OrganizationApiKeyRequest } from "@bitwarden/common/admin-console/models/request/organization-api-key.request"; import { OrganizationApiKeyRequest } from "@bitwarden/common/admin-console/models/request/organization-api-key.request";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response"; import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";

View File

@ -1,7 +1,7 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { UserVerificationModule } from "../../auth/shared/components/user-verification";
import { LooseComponentsModule, SharedModule } from "../../shared"; import { LooseComponentsModule, SharedModule } from "../../shared";
import { UserVerificationModule } from "../../shared/components/user-verification";
import { AdjustSubscription } from "./adjust-subscription.component"; import { AdjustSubscription } from "./adjust-subscription.component";
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";

View File

@ -1,6 +1,6 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response"; import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";

View File

@ -2,7 +2,7 @@ import { Component, Input } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
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";

View File

@ -1,26 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()">
<h2 class="tw-my-6 tw-px-3.5 tw-font-semibold" id="modalTitle | i18n ">
{{ modalTitle | i18n | uppercase }}
</h2>
<div class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-p-3.5">
{{ confirmDescription | i18n }}
</div>
<div class="tw-p-3.5">
<app-user-verification ngDefaultControl [formControl]="secret" name="secret">
</app-user-verification>
</div>
<div
class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background-alt tw-p-3.5"
>
<button type="button" bitButton buttonType="primary" type="submit" appBlurClick>
<span>{{ confirmButtonText | i18n }}</span>
</button>
<button type="button" bitButton buttonType="secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@ -1,8 +0,0 @@
import { Component } from "@angular/core";
import { UserVerificationPromptComponent as BaseUserVerificationPrompt } from "@bitwarden/angular/auth/components/user-verification-prompt.component";
@Component({
templateUrl: "user-verification-prompt.component.html",
})
export class UserVerificationPromptComponent extends BaseUserVerificationPrompt {}

View File

@ -1,46 +0,0 @@
<ng-container *ngIf="!usesKeyConnector">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[formControl]="secret"
required
appAutofocus
appInputVerbatim
/>
<small class="form-text text-muted">{{ "confirmIdentity" | i18n }}</small>
</ng-container>
<ng-container *ngIf="usesKeyConnector">
<div class="form-group">
<label class="d-block">{{ "sendVerificationCode" | i18n }}</label>
<button
type="button"
class="btn btn-outline-secondary"
(click)="requestOTP()"
[disabled]="disableRequestOTP"
>
{{ "sendCode" | i18n }}
</button>
<span class="ml-2 text-success" role="alert" @sent *ngIf="sentCode">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "codeSent" | i18n }}
</span>
</div>
<div class="form-group">
<label for="verificationCode">{{ "verificationCode" | i18n }}</label>
<input
id="verificationCode"
type="input"
name="verificationCode"
class="form-control"
[formControl]="secret"
required
appAutofocus
appInputVerbatim
/>
<small class="form-text text-muted">{{ "confirmIdentity" | i18n }}</small>
</div>
</ng-container>

View File

@ -43,6 +43,7 @@ import { TwoFactorVerifyComponent } from "../auth/settings/two-factor-verify.com
import { TwoFactorWebAuthnComponent } from "../auth/settings/two-factor-webauthn.component"; import { TwoFactorWebAuthnComponent } from "../auth/settings/two-factor-webauthn.component";
import { TwoFactorYubiKeyComponent } from "../auth/settings/two-factor-yubikey.component"; import { TwoFactorYubiKeyComponent } from "../auth/settings/two-factor-yubikey.component";
import { VerifyEmailComponent } from "../auth/settings/verify-email.component"; import { VerifyEmailComponent } from "../auth/settings/verify-email.component";
import { UserVerificationModule } from "../auth/shared/components/user-verification";
import { SsoComponent } from "../auth/sso.component"; import { SsoComponent } from "../auth/sso.component";
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component"; import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
import { TwoFactorComponent } from "../auth/two-factor.component"; import { TwoFactorComponent } from "../auth/two-factor.component";
@ -109,7 +110,6 @@ import { AttachmentsComponent as OrgAttachmentsComponent } from "../vault/org-va
import { CollectionsComponent as OrgCollectionsComponent } from "../vault/org-vault/collections.component"; import { CollectionsComponent as OrgCollectionsComponent } from "../vault/org-vault/collections.component";
import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component"; import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component";
import { UserVerificationModule } from "./components/user-verification";
import { SharedModule } from "./shared.module"; import { SharedModule } from "./shared.module";
// Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left. // Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left.
@ -234,6 +234,7 @@ import { SharedModule } from "./shared.module";
LowKdfComponent, LowKdfComponent,
], ],
exports: [ exports: [
UserVerificationModule,
PremiumBadgeComponent, PremiumBadgeComponent,
AcceptEmergencyComponent, AcceptEmergencyComponent,
AcceptOrganizationComponent, AcceptOrganizationComponent,

View File

@ -1,12 +1,12 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms"; import { UntypedFormBuilder } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component"; import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { EncryptedExportType } from "@bitwarden/common/enums"; import { EncryptedExportType } from "@bitwarden/common/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@ -15,7 +15,7 @@ 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 { VaultExportServiceAbstraction } from "@bitwarden/exporter/vault-export"; import { VaultExportServiceAbstraction } from "@bitwarden/exporter/vault-export";
import { UserVerificationPromptComponent } from "../../shared/components/user-verification"; import { openUserVerificationPrompt } from "../../auth/shared/components/user-verification";
@Component({ @Component({
selector: "app-export", selector: "app-export",
@ -37,7 +37,6 @@ export class ExportComponent extends BaseExportComponent {
userVerificationService: UserVerificationService, userVerificationService: UserVerificationService,
formBuilder: UntypedFormBuilder, formBuilder: UntypedFormBuilder,
fileDownloadService: FileDownloadService, fileDownloadService: FileDownloadService,
private modalService: ModalService,
dialogService: DialogServiceAbstraction dialogService: DialogServiceAbstraction
) { ) {
super( super(
@ -101,8 +100,7 @@ export class ExportComponent extends BaseExportComponent {
confirmDescription = "encExportKeyWarningDesc"; confirmDescription = "encExportKeyWarningDesc";
} }
const ref = this.modalService.open(UserVerificationPromptComponent, { const ref = openUserVerificationPrompt(this.dialogService, {
allowMultipleModals: true,
data: { data: {
confirmDescription: confirmDescription, confirmDescription: confirmDescription,
confirmButtonText: "exportVault", confirmButtonText: "exportVault",
@ -114,7 +112,7 @@ export class ExportComponent extends BaseExportComponent {
return; return;
} }
return ref.onClosedPromise(); return firstValueFrom(ref.closed);
} }
get isFileEncryptedExport() { get isFileEncryptedExport() {

View File

@ -22,6 +22,19 @@
<input bitInput appAutofocus formControlName="name" /> <input bitInput appAutofocus formControlName="name" />
</bit-form-field> </bit-form-field>
<bit-form-field *ngIf="showOrgSelector">
<bit-label>{{ "organization" | i18n }}</bit-label>
<bit-select bitInput formControlName="selectedOrg">
<bit-option
*ngFor="let org of organizations$ | async"
icon="bwi-business"
[value]="org.id"
[label]="org.name"
>
</bit-option>
</bit-select>
</bit-form-field>
<bit-form-field> <bit-form-field>
<bit-label>{{ "externalId" | i18n }}</bit-label> <bit-label>{{ "externalId" | i18n }}</bit-label>
<input bitInput formControlName="externalId" /> <input bitInput formControlName="externalId" />

View File

@ -1,7 +1,16 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs"; import {
combineLatest,
map,
Observable,
of,
shareReplay,
Subject,
switchMap,
takeUntil,
} from "rxjs";
import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
@ -10,6 +19,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionResponse } from "@bitwarden/common/vault/models/response/collection.response";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { BitValidators } from "@bitwarden/components"; import { BitValidators } from "@bitwarden/components";
@ -35,9 +46,16 @@ export interface CollectionDialogParams {
organizationId: string; organizationId: string;
initialTab?: CollectionDialogTabType; initialTab?: CollectionDialogTabType;
parentCollectionId?: string; parentCollectionId?: string;
showOrgSelector?: boolean;
collectionIds?: string[];
} }
export enum CollectionDialogResult { export interface CollectionDialogResult {
action: CollectionDialogAction;
collection: CollectionResponse;
}
export enum CollectionDialogAction {
Saved = "saved", Saved = "saved",
Canceled = "canceled", Canceled = "canceled",
Deleted = "deleted", Deleted = "deleted",
@ -48,6 +66,7 @@ export enum CollectionDialogResult {
}) })
export class CollectionDialogComponent implements OnInit, OnDestroy { export class CollectionDialogComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
protected organizations$: Observable<Organization[]>;
protected tabIndex: CollectionDialogTabType; protected tabIndex: CollectionDialogTabType;
protected loading = true; protected loading = true;
@ -56,11 +75,13 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
protected nestOptions: CollectionView[] = []; protected nestOptions: CollectionView[] = [];
protected accessItems: AccessItemView[] = []; protected accessItems: AccessItemView[] = [];
protected deletedParentName: string | undefined; protected deletedParentName: string | undefined;
protected showOrgSelector = false;
protected formGroup = this.formBuilder.group({ protected formGroup = this.formBuilder.group({
name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]], name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]],
externalId: "", externalId: "",
parent: undefined as string | undefined, parent: undefined as string | undefined,
access: [[] as AccessItemValue[]], access: [[] as AccessItemValue[]],
selectedOrg: "",
}); });
protected PermissionMode = PermissionMode; protected PermissionMode = PermissionMode;
@ -79,8 +100,31 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info; this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info;
} }
ngOnInit() { async ngOnInit() {
const organization$ = of(this.organizationService.get(this.params.organizationId)).pipe( // Opened from the individual vault
if (this.params.showOrgSelector) {
this.showOrgSelector = true;
this.formGroup.controls.selectedOrg.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((id) => this.loadOrg(id, this.params.collectionIds));
this.organizations$ = this.organizationService.organizations$.pipe(
map((orgs) =>
orgs
.filter((o) => o.canCreateNewCollections)
.sort(Utils.getSortFunction(this.i18nService, "name"))
)
);
// patchValue will trigger a call to loadOrg() in this case, so no need to call it again here
this.formGroup.patchValue({ selectedOrg: this.params.organizationId });
} else {
// Opened from the org vault
this.formGroup.patchValue({ selectedOrg: this.params.organizationId });
this.loadOrg(this.params.organizationId, this.params.collectionIds);
}
}
async loadOrg(orgId: string, collectionIds: string[]) {
const organization$ = of(this.organizationService.get(orgId)).pipe(
shareReplay({ refCount: true, bufferSize: 1 }) shareReplay({ refCount: true, bufferSize: 1 })
); );
const groups$ = organization$.pipe( const groups$ = organization$.pipe(
@ -89,20 +133,19 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
return of([] as GroupView[]); return of([] as GroupView[]);
} }
return this.groupService.getAll(this.params.organizationId); return this.groupService.getAll(orgId);
}) })
); );
combineLatest({ combineLatest({
organization: organization$, organization: organization$,
collections: this.collectionService.getAll(this.params.organizationId), collections: this.collectionService.getAll(orgId),
collectionDetails: this.params.collectionId collectionDetails: this.params.collectionId
? this.collectionService.get(this.params.organizationId, this.params.collectionId) ? this.collectionService.get(orgId, this.params.collectionId)
: of(null), : of(null),
groups: groups$, groups: groups$,
users: this.organizationUserService.getAllUsers(this.params.organizationId), users: this.organizationUserService.getAllUsers(orgId),
}) })
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
.subscribe(({ organization, collections, collectionDetails, groups, users }) => { .subscribe(({ organization, collections, collectionDetails, groups, users }) => {
this.organization = organization; this.organization = organization;
this.accessItems = [].concat( this.accessItems = [].concat(
@ -110,6 +153,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
users.data.map(mapUserToAccessItemView) users.data.map(mapUserToAccessItemView)
); );
if (collectionIds) {
collections = collections.filter((c) => collectionIds.includes(c.id));
}
if (this.params.collectionId) { if (this.params.collectionId) {
this.collection = collections.find((c) => c.id === this.collectionId); this.collection = collections.find((c) => c.id === this.collectionId);
this.nestOptions = collections.filter((c) => c.id !== this.collectionId); this.nestOptions = collections.filter((c) => c.id !== this.collectionId);
@ -149,7 +196,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
} }
protected async cancel() { protected async cancel() {
this.close(CollectionDialogResult.Canceled); this.close(CollectionDialogAction.Canceled);
} }
protected submit = async () => { protected submit = async () => {
@ -168,7 +215,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
const collectionView = new CollectionAdminView(); const collectionView = new CollectionAdminView();
collectionView.id = this.params.collectionId; collectionView.id = this.params.collectionId;
collectionView.organizationId = this.params.organizationId; collectionView.organizationId = this.formGroup.controls.selectedOrg.value;
collectionView.externalId = this.formGroup.controls.externalId.value; collectionView.externalId = this.formGroup.controls.externalId.value;
collectionView.groups = this.formGroup.controls.access.value collectionView.groups = this.formGroup.controls.access.value
.filter((v) => v.type === AccessItemType.Group) .filter((v) => v.type === AccessItemType.Group)
@ -184,7 +231,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
collectionView.name = this.formGroup.controls.name.value; collectionView.name = this.formGroup.controls.name.value;
} }
await this.collectionService.save(collectionView); const savedCollection = await this.collectionService.save(collectionView);
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"success", "success",
@ -195,7 +242,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
) )
); );
this.close(CollectionDialogResult.Saved); this.close(CollectionDialogAction.Saved, savedCollection);
}; };
protected delete = async () => { protected delete = async () => {
@ -217,7 +264,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
this.i18nService.t("deletedCollectionId", this.collection?.name) this.i18nService.t("deletedCollectionId", this.collection?.name)
); );
this.close(CollectionDialogResult.Deleted); this.close(CollectionDialogAction.Deleted);
}; };
ngOnDestroy(): void { ngOnDestroy(): void {
@ -225,8 +272,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
this.destroy$.complete(); this.destroy$.complete();
} }
private close(result: CollectionDialogResult) { private close(action: CollectionDialogAction, collection?: CollectionResponse) {
this.dialogRef.close(result); this.dialogRef.close({ action, collection } as CollectionDialogResult);
} }
} }

View File

@ -46,7 +46,7 @@ export class CollectionAdminService {
return view; return view;
} }
async save(collection: CollectionAdminView): Promise<unknown> { async save(collection: CollectionAdminView): Promise<CollectionResponse> {
const request = await this.encrypt(collection); const request = await this.encrypt(collection);
let response: CollectionResponse; let response: CollectionResponse;
@ -61,9 +61,7 @@ export class CollectionAdminService {
); );
} }
// TODO: Implement upsert when in PS-1083: Collection Service refactors return response;
// await this.collectionService.upsert(data);
return;
} }
async delete(organizationId: string, collectionId: string): Promise<void> { async delete(organizationId: string, collectionId: string): Promise<void> {

View File

@ -32,7 +32,6 @@ import { OrganizationOptionsComponent } from "./organization-options.component";
export class VaultFilterComponent implements OnInit, OnDestroy { export class VaultFilterComponent implements OnInit, OnDestroy {
filters?: VaultFilterList; filters?: VaultFilterList;
@Input() activeFilter: VaultFilter = new VaultFilter(); @Input() activeFilter: VaultFilter = new VaultFilter();
@Output() onAddFolder = new EventEmitter<never>();
@Output() onEditFolder = new EventEmitter<FolderFilter>(); @Output() onEditFolder = new EventEmitter<FolderFilter>();
@Input() searchText = ""; @Input() searchText = "";
@ -142,10 +141,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
filter.selectedCollectionNode = collectionNode; filter.selectedCollectionNode = collectionNode;
}; };
addFolder = async (): Promise<void> => {
this.onAddFolder.emit();
};
editFolder = async (folder: FolderFilter): Promise<void> => { editFolder = async (folder: FolderFilter): Promise<void> => {
this.onEditFolder.emit(folder); this.onEditFolder.emit(folder);
}; };
@ -249,10 +244,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
text: "editFolder", text: "editFolder",
action: this.editFolder, action: this.editFolder,
}, },
add: {
text: "Add Folder",
action: this.addFolder,
},
}; };
return folderFilterSection; return folderFilterSection;
} }

View File

@ -34,16 +34,6 @@
<h3 *ngIf="!headerInfo.isSelectable" class="filter-title"> <h3 *ngIf="!headerInfo.isSelectable" class="filter-title">
&nbsp;{{ headerNode.node.name | i18n }} &nbsp;{{ headerNode.node.name | i18n }}
</h3> </h3>
<button
type="button"
*ngIf="showAddButton"
(click)="onAdd()"
class="text-muted ml-auto add-button"
appA11yTitle="{{ addInfo.text | i18n }}"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</button>
</div> </div>
<ul <ul
id="{{ headerNode.node.name }}-filters" id="{{ headerNode.node.name }}-filters"

View File

@ -87,10 +87,6 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
return this.section.add; return this.section.add;
} }
get showAddButton() {
return this.section.add && !this.section.add.route;
}
get showAddLink() { get showAddLink() {
return this.section.add && this.section.add.route; return this.section.add && this.section.add.route;
} }

View File

@ -40,9 +40,31 @@
</div> </div>
<div *ngIf="filter.type !== 'trash'" class="tw-shrink-0"> <div *ngIf="filter.type !== 'trash'" class="tw-shrink-0">
<button type="button" bitButton buttonType="primary" (click)="addCipher()"> <div appListDropdown>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i> <button
{{ "newItem" | i18n }} bitButton
</button> buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
{{ "new" | i18n }}<i class="bwi bwi-angle-down tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher()">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
{{ "item" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button *ngIf="canCreateCollections" type="button" bitMenuItem (click)="addCollection()">
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</div>
</div> </div>
</div> </div>

View File

@ -39,11 +39,26 @@ export class VaultHeaderComponent {
*/ */
@Input() collection?: TreeNode<CollectionView>; @Input() collection?: TreeNode<CollectionView>;
/**
* Whether 'Collection' option is shown in the 'New' dropdown
*/
@Input() canCreateCollections: boolean;
/** /**
* Emits an event when the new item button is clicked in the header * Emits an event when the new item button is clicked in the header
*/ */
@Output() onAddCipher = new EventEmitter<void>(); @Output() onAddCipher = new EventEmitter<void>();
/**
* Emits an event when the new collection button is clicked in the 'New' dropdown menu
*/
@Output() onAddCollection = new EventEmitter<null>();
/**
* Emits an event when the new folder button is clicked in the 'New' dropdown menu
*/
@Output() onAddFolder = new EventEmitter<null>();
constructor(private i18nService: I18nService) {} constructor(private i18nService: I18nService) {}
/** /**
@ -115,4 +130,12 @@ export class VaultHeaderComponent {
protected addCipher() { protected addCipher() {
this.onAddCipher.emit(); this.onAddCipher.emit();
} }
async addFolder(): Promise<void> {
this.onAddFolder.emit();
}
async addCollection(): Promise<void> {
this.onAddCollection.emit();
}
} }

View File

@ -9,7 +9,6 @@
[activeFilter]="activeFilter" [activeFilter]="activeFilter"
[searchText]="currentSearchText$ | async" [searchText]="currentSearchText$ | async"
(searchTextChanged)="filterSearchText($event)" (searchTextChanged)="filterSearchText($event)"
(onAddFolder)="addFolder()"
(onEditFolder)="editFolder($event)" (onEditFolder)="editFolder($event)"
></app-vault-filter> ></app-vault-filter>
</div> </div>
@ -21,8 +20,11 @@
[filter]="filter" [filter]="filter"
[loading]="refreshing && !performingInitialLoad" [loading]="refreshing && !performingInitialLoad"
[organizations]="allOrganizations" [organizations]="allOrganizations"
[canCreateCollections]="canCreateCollections"
[collection]="selectedCollection" [collection]="selectedCollection"
(onAddCipher)="addCipher()" (onAddCipher)="addCipher()"
(onAddCollection)="addCollection()"
(onAddFolder)="addFolder()"
></app-vault-header> ></app-vault-header>
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle"> <app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
{{ trashCleanupWarning }} {{ trashCleanupWarning }}

View File

@ -54,11 +54,14 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { Icons } from "@bitwarden/components"; import { Icons } from "@bitwarden/components";
import { UpdateKeyComponent } from "../../settings/update-key.component"; import { UpdateKeyComponent } from "../../settings/update-key.component";
import { CollectionDialogAction, openCollectionDialog } from "../components/collection-dialog";
import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { getNestedCollectionTree } from "../utils/collection-utils"; import { getNestedCollectionTree } from "../utils/collection-utils";
@ -140,6 +143,7 @@ export class VaultComponent implements OnInit, OnDestroy {
protected collections: CollectionView[]; protected collections: CollectionView[];
protected isEmpty: boolean; protected isEmpty: boolean;
protected selectedCollection: TreeNode<CollectionView> | undefined; protected selectedCollection: TreeNode<CollectionView> | undefined;
protected canCreateCollections = false;
protected currentSearchText$: Observable<string>; protected currentSearchText$: Observable<string>;
private searchText$ = new Subject<string>(); private searchText$ = new Subject<string>();
@ -234,12 +238,9 @@ export class VaultComponent implements OnInit, OnDestroy {
const canAccessPremium$ = Utils.asyncToObservable(() => const canAccessPremium$ = Utils.asyncToObservable(() =>
this.stateService.getCanAccessPremium() this.stateService.getCanAccessPremium()
).pipe(shareReplay({ refCount: true, bufferSize: 1 })); ).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
const allCollections$ = Utils.asyncToObservable(() => const allCollections$ = Utils.asyncToObservable(() => this.collectionService.getAllDecrypted());
this.collectionService.getAllDecrypted()
).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
const nestedCollections$ = allCollections$.pipe( const nestedCollections$ = allCollections$.pipe(
map((collections) => getNestedCollectionTree(collections)), map((collections) => getNestedCollectionTree(collections))
shareReplay({ refCount: true, bufferSize: 1 })
); );
this.searchText$ this.searchText$
@ -384,6 +385,8 @@ export class VaultComponent implements OnInit, OnDestroy {
this.collections = collections; this.collections = collections;
this.selectedCollection = selectedCollection; this.selectedCollection = selectedCollection;
this.canCreateCollections = allOrganizations?.some((o) => o.canCreateNewCollections);
this.showBulkMove = this.showBulkMove =
filter.type !== "trash" && filter.type !== "trash" &&
(filter.organizationId === undefined || filter.organizationId === Unassigned); (filter.organizationId === undefined || filter.organizationId === Unassigned);
@ -639,6 +642,32 @@ export class VaultComponent implements OnInit, OnDestroy {
return childComponent; return childComponent;
} }
async addCollection() {
const dialog = openCollectionDialog(this.dialogService, {
data: {
organizationId: this.allOrganizations
.filter((o) => o.canCreateNewCollections)
.sort(Utils.getSortFunction(this.i18nService, "name"))[0].id,
parentCollectionId: this.filter.collectionId,
showOrgSelector: true,
collectionIds: this.allCollections.map((c) => c.id),
},
});
const result = await lastValueFrom(dialog.closed);
if (result.action === CollectionDialogAction.Saved) {
if (result.collection) {
// Update CollectionService with the new collection
const c = new CollectionData(result.collection as CollectionDetailsResponse);
await this.collectionService.upsert(c);
}
this.refresh();
} else if (result.action === CollectionDialogAction.Deleted) {
// TODO: Remove collection from collectionService when collection
// deletion is implemented in the individual vault in AC-1347
this.refresh();
}
}
async cloneCipher(cipher: CipherView) { async cloneCipher(cipher: CipherView) {
const component = await this.editCipher(cipher); const component = await this.editCipher(cipher);
component.cloneMode = true; component.cloneMode = true;

View File

@ -60,7 +60,7 @@ import { openEntityEventsDialog } from "../../admin-console/organizations/manage
import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model"; import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
import { import {
CollectionDialogResult, CollectionDialogAction,
CollectionDialogTabType, CollectionDialogTabType,
openCollectionDialog, openCollectionDialog,
} from "../components/collection-dialog"; } from "../components/collection-dialog";
@ -866,7 +866,10 @@ export class VaultComponent implements OnInit, OnDestroy {
}); });
const result = await lastValueFrom(dialog.closed); const result = await lastValueFrom(dialog.closed);
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) { if (
result.action === CollectionDialogAction.Saved ||
result.action === CollectionDialogAction.Deleted
) {
this.refresh(); this.refresh();
} }
} }
@ -877,7 +880,10 @@ export class VaultComponent implements OnInit, OnDestroy {
}); });
const result = await lastValueFrom(dialog.closed); const result = await lastValueFrom(dialog.closed);
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) { if (
result.action === CollectionDialogAction.Saved ||
result.action === CollectionDialogAction.Deleted
) {
this.refresh(); this.refresh();
} }
} }

View File

@ -964,8 +964,14 @@
"confirmVaultExport": { "confirmVaultExport": {
"message": "Confirm vault export" "message": "Confirm vault export"
}, },
"confirmSecretsExport": {
"message": "Confirm secrets export"
},
"exportWarningDesc": { "exportWarningDesc": {
"message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it." "message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it."
},
"exportSecretsWarningDesc": {
"message": "This export contains your secrets data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it."
}, },
"encExportKeyWarningDesc": { "encExportKeyWarningDesc": {
"message": "This export encrypts your data using your account's encryption key. If you ever rotate your account's encryption key you should export again since you will not be able to decrypt this export file." "message": "This export encrypts your data using your account's encryption key. If you ever rotate your account's encryption key you should export again since you will not be able to decrypt this export file."
@ -979,6 +985,9 @@
"exportVault": { "exportVault": {
"message": "Export vault" "message": "Export vault"
}, },
"exportSecrets": {
"message": "Export secrets"
},
"fileFormat": { "fileFormat": {
"message": "File format" "message": "File format"
}, },
@ -2657,6 +2666,12 @@
"failedLogin2fa": { "failedLogin2fa": {
"message": "Login attempt failed with incorrect two-step login." "message": "Login attempt failed with incorrect two-step login."
}, },
"incorrectPassword": {
"message": "Incorrect password"
},
"incorrectCode": {
"message": "Incorrect code"
},
"exportedVault": { "exportedVault": {
"message": "Vault exported" "message": "Vault exported"
}, },
@ -6182,10 +6197,6 @@
"message": "Access token created and copied to clipboard", "message": "Access token created and copied to clipboard",
"description": "Notification to inform the user that the access token has been created and copied to the clipboard." "description": "Notification to inform the user that the access token has been created and copied to the clipboard."
}, },
"accessTokenPermissionsBetaNotification": {
"message": "Permissions management is unavailable for beta.",
"description": "Notification to inform the user that the feature for managing access token permissions is not available in the beta version."
},
"revokeAccessToken": { "revokeAccessToken": {
"message": "Revoke access token", "message": "Revoke access token",
"description": "Invalidates / cancels an access token and as such removes access to secrets for the client application." "description": "Invalidates / cancels an access token and as such removes access to secrets for the client application."

View File

@ -7,7 +7,7 @@
granteeType="people" granteeType="people"
[label]="'people' | i18n" [label]="'people' | i18n"
[hint]="'projectPeopleSelectHint' | i18n" [hint]="'projectPeopleSelectHint' | i18n"
[columnTitle]="'groupSlashUser' | i18n" [columnTitle]="'name' | i18n"
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n" [emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n"
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"

View File

@ -32,7 +32,6 @@
</label> </label>
</th> </th>
<th bitCell>{{ "name" | i18n }}</th> <th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "permissions" | i18n }}</th>
<th bitCell>{{ "expires" | i18n }}</th> <th bitCell>{{ "expires" | i18n }}</th>
<th bitCell>{{ "lastEdited" | i18n }}</th> <th bitCell>{{ "lastEdited" | i18n }}</th>
<th bitCell class="tw-w-0"> <th bitCell class="tw-w-0">
@ -57,7 +56,6 @@
/> />
</td> </td>
<td bitCell>{{ token.name }}</td> <td bitCell>{{ token.name }}</td>
<td bitCell>{{ permission(token) | i18n }}</td>
<td bitCell> <td bitCell>
{{ token.expireAt === null ? ("never" | i18n) : (token.expireAt | date : "medium") }} {{ token.expireAt === null ? ("never" | i18n) : (token.expireAt | date : "medium") }}
</td> </td>

View File

@ -39,8 +39,4 @@ export class AccessListComponent {
const selected = this.tokens.filter((s) => this.selection.selected.includes(s.id)); const selected = this.tokens.filter((s) => this.selection.selected.includes(s.id));
this.revokeAccessTokensEvent.emit(selected); this.revokeAccessTokensEvent.emit(selected);
} }
protected permission(token: AccessTokenView) {
return "canRead";
}
} }

View File

@ -1,14 +1,24 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { combineLatestWith, Observable, startWith, switchMap } from "rxjs"; import {
combineLatestWith,
firstValueFrom,
Observable,
startWith,
Subject,
switchMap,
takeUntil,
} from "rxjs";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserVerificationPromptComponent } from "@bitwarden/web-vault/app/shared/components/user-verification"; import { openUserVerificationPrompt } from "@bitwarden/web-vault/app/auth/shared/components/user-verification";
import { ServiceAccountView } from "../../models/view/service-account.view";
import { AccessTokenView } from "../models/view/access-token.view"; import { AccessTokenView } from "../models/view/access-token.view";
import { ServiceAccountService } from "../service-account.service";
import { AccessService } from "./access.service"; import { AccessService } from "./access.service";
import { AccessTokenCreateDialogComponent } from "./dialogs/access-token-create-dialog.component"; import { AccessTokenCreateDialogComponent } from "./dialogs/access-token-create-dialog.component";
@ -17,11 +27,11 @@ import { AccessTokenCreateDialogComponent } from "./dialogs/access-token-create-
selector: "sm-access-tokens", selector: "sm-access-tokens",
templateUrl: "./access-tokens.component.html", templateUrl: "./access-tokens.component.html",
}) })
export class AccessTokenComponent implements OnInit { export class AccessTokenComponent implements OnInit, OnDestroy {
accessTokens$: Observable<AccessTokenView[]>; accessTokens$: Observable<AccessTokenView[]>;
private serviceAccountId: string; private destroy$ = new Subject<void>();
private organizationId: string; private serviceAccountView: ServiceAccountView;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -29,19 +39,39 @@ export class AccessTokenComponent implements OnInit {
private dialogService: DialogServiceAbstraction, private dialogService: DialogServiceAbstraction,
private modalService: ModalService, private modalService: ModalService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService private i18nService: I18nService,
private serviceAccountService: ServiceAccountService
) {} ) {}
ngOnInit() { ngOnInit() {
this.accessTokens$ = this.accessService.accessToken$.pipe( this.accessTokens$ = this.accessService.accessToken$.pipe(
startWith(null), startWith(null),
combineLatestWith(this.route.params), combineLatestWith(this.route.params),
switchMap(async ([_, params]) => { switchMap(async ([_, params]) =>
this.organizationId = params.organizationId; this.accessService.getAccessTokens(params.organizationId, params.serviceAccountId)
this.serviceAccountId = params.serviceAccountId; )
return await this.getAccessTokens();
})
); );
this.serviceAccountService.serviceAccount$
.pipe(
startWith(null),
combineLatestWith(this.route.params),
switchMap(([_, params]) =>
this.serviceAccountService.getByServiceAccountId(
params.serviceAccountId,
params.organizationId
)
),
takeUntil(this.destroy$)
)
.subscribe((serviceAccountView) => {
this.serviceAccountView = serviceAccountView;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
} }
protected async revoke(tokens: AccessTokenView[]) { protected async revoke(tokens: AccessTokenView[]) {
@ -59,7 +89,7 @@ export class AccessTokenComponent implements OnInit {
} }
await this.accessService.revokeAccessTokens( await this.accessService.revokeAccessTokens(
this.serviceAccountId, this.serviceAccountView.id,
tokens.map((t) => t.id) tokens.map((t) => t.id)
); );
@ -69,14 +99,12 @@ export class AccessTokenComponent implements OnInit {
protected openNewAccessTokenDialog() { protected openNewAccessTokenDialog() {
AccessTokenCreateDialogComponent.openNewAccessTokenDialog( AccessTokenCreateDialogComponent.openNewAccessTokenDialog(
this.dialogService, this.dialogService,
this.serviceAccountId, this.serviceAccountView
this.organizationId
); );
} }
private verifyUser() { private verifyUser() {
const ref = this.modalService.open(UserVerificationPromptComponent, { const ref = openUserVerificationPrompt(this.dialogService, {
allowMultipleModals: true,
data: { data: {
confirmDescription: "revokeAccessTokenDesc", confirmDescription: "revokeAccessTokenDesc",
confirmButtonText: "revokeAccessToken", confirmButtonText: "revokeAccessToken",
@ -88,10 +116,6 @@ export class AccessTokenComponent implements OnInit {
return; return;
} }
return ref.onClosedPromise(); return firstValueFrom(ref.closed);
}
private async getAccessTokens(): Promise<AccessTokenView[]> {
return await this.accessService.getAccessTokens(this.organizationId, this.serviceAccountId);
} }
} }

View File

@ -12,19 +12,6 @@
<bit-label>{{ "name" | i18n }}</bit-label> <bit-label>{{ "name" | i18n }}</bit-label>
<input bitInput formControlName="name" /> <input bitInput formControlName="name" />
</bit-form-field> </bit-form-field>
<div class="tw-mb-6">
<bit-form-field class="tw-mb-0">
<bit-label>{{ "permissions" | i18n }}</bit-label>
<select bitInput disabled>
<option selected value="canRead">
{{ "canRead" | i18n }}
</option>
</select>
</bit-form-field>
<span class="tw-text-sm tw-text-muted">
{{ "accessTokenPermissionsBetaNotification" | i18n }}
</span>
</div>
<sm-expiration-options <sm-expiration-options
formControlName="expirationDateControl" formControlName="expirationDateControl"
[expirationDayOptions]="expirationDayOptions" [expirationDayOptions]="expirationDayOptions"

View File

@ -3,7 +3,6 @@ import { Component, Inject, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms"; import { FormControl, FormGroup, Validators } from "@angular/forms";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { BitValidators } from "@bitwarden/components"; import { BitValidators } from "@bitwarden/components";
import { ServiceAccountView } from "../../../models/view/service-account.view"; import { ServiceAccountView } from "../../../models/view/service-account.view";
@ -13,7 +12,6 @@ import { AccessService } from "../access.service";
import { AccessTokenDetails, AccessTokenDialogComponent } from "./access-token-dialog.component"; import { AccessTokenDetails, AccessTokenDialogComponent } from "./access-token-dialog.component";
export interface AccessTokenOperation { export interface AccessTokenOperation {
organizationId: string;
serviceAccountView: ServiceAccountView; serviceAccountView: ServiceAccountView;
} }
@ -35,17 +33,12 @@ export class AccessTokenCreateDialogComponent implements OnInit {
constructor( constructor(
public dialogRef: DialogRef, public dialogRef: DialogRef,
@Inject(DIALOG_DATA) public data: AccessTokenOperation, @Inject(DIALOG_DATA) public data: AccessTokenOperation,
private i18nService: I18nService,
private dialogService: DialogServiceAbstraction, private dialogService: DialogServiceAbstraction,
private accessService: AccessService private accessService: AccessService
) {} ) {}
async ngOnInit() { async ngOnInit() {
if ( if (!this.data.serviceAccountView) {
!this.data.organizationId ||
!this.data.serviceAccountView?.id ||
!this.data.serviceAccountView?.name
) {
this.dialogRef.close(); this.dialogRef.close();
throw new Error( throw new Error(
`The access token create dialog was not called with the appropriate operation values.` `The access token create dialog was not called with the appropriate operation values.`
@ -62,7 +55,7 @@ export class AccessTokenCreateDialogComponent implements OnInit {
accessTokenView.name = this.formGroup.value.name; accessTokenView.name = this.formGroup.value.name;
accessTokenView.expireAt = this.formGroup.value.expirationDateControl; accessTokenView.expireAt = this.formGroup.value.expirationDateControl;
const accessToken = await this.accessService.createAccessToken( const accessToken = await this.accessService.createAccessToken(
this.data.organizationId, this.data.serviceAccountView.organizationId,
this.data.serviceAccountView.id, this.data.serviceAccountView.id,
accessTokenView accessTokenView
); );
@ -90,18 +83,11 @@ export class AccessTokenCreateDialogComponent implements OnInit {
static openNewAccessTokenDialog( static openNewAccessTokenDialog(
dialogService: DialogServiceAbstraction, dialogService: DialogServiceAbstraction,
serviceAccountId: string, serviceAccountView: ServiceAccountView
organizationId: string
) { ) {
// TODO once service account names are implemented in service account contents page pass in here.
const serviceAccountView = new ServiceAccountView();
serviceAccountView.id = serviceAccountId;
serviceAccountView.name = "placeholder";
return dialogService.open<unknown, AccessTokenOperation>(AccessTokenCreateDialogComponent, { return dialogService.open<unknown, AccessTokenOperation>(AccessTokenCreateDialogComponent, {
data: { data: {
organizationId: organizationId, serviceAccountView,
serviceAccountView: serviceAccountView,
}, },
}); });
} }

View File

@ -7,7 +7,7 @@
granteeType="people" granteeType="people"
[label]="'people' | i18n" [label]="'people' | i18n"
[hint]="'projectPeopleSelectHint' | i18n" [hint]="'projectPeopleSelectHint' | i18n"
[columnTitle]="'groupSlashUser' | i18n" [columnTitle]="'name' | i18n"
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n" [emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n"
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"

View File

@ -15,6 +15,8 @@ import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ServiceAccountView } from "../models/view/service-account.view";
import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component"; import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component";
import { ServiceAccountService } from "./service-account.service"; import { ServiceAccountService } from "./service-account.service";
@ -32,6 +34,7 @@ export class ServiceAccountComponent implements OnInit, OnDestroy {
startWith(null) startWith(null)
); );
private serviceAccountView: ServiceAccountView;
protected serviceAccount$ = combineLatest([this.route.params, this.onChange$]).pipe( protected serviceAccount$ = combineLatest([this.route.params, this.onChange$]).pipe(
switchMap(([params, _]) => switchMap(([params, _]) =>
this.serviceAccountService.getByServiceAccountId( this.serviceAccountService.getByServiceAccountId(
@ -61,9 +64,8 @@ export class ServiceAccountComponent implements OnInit, OnDestroy {
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { this.serviceAccount$.pipe(takeUntil(this.destroy$)).subscribe((serviceAccountView) => {
this.serviceAccountId = params.serviceAccountId; this.serviceAccountView = serviceAccountView;
this.organizationId = params.organizationId;
}); });
} }
@ -75,8 +77,7 @@ export class ServiceAccountComponent implements OnInit, OnDestroy {
protected openNewAccessTokenDialog() { protected openNewAccessTokenDialog() {
AccessTokenCreateDialogComponent.openNewAccessTokenDialog( AccessTokenCreateDialogComponent.openNewAccessTokenDialog(
this.dialogService, this.dialogService,
this.serviceAccountId, this.serviceAccountView
this.organizationId
); );
} }
} }

View File

@ -1,15 +1,15 @@
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms"; import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { Subject, switchMap, takeUntil } from "rxjs"; import { firstValueFrom, Subject, switchMap, takeUntil } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
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 { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.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 { UserVerificationPromptComponent } from "@bitwarden/web-vault/app/shared/components/user-verification"; import { openUserVerificationPrompt } from "@bitwarden/web-vault/app/auth/shared/components/user-verification";
import { SecretsManagerPortingApiService } from "../services/sm-porting-api.service"; import { SecretsManagerPortingApiService } from "../services/sm-porting-api.service";
import { SecretsManagerPortingService } from "../services/sm-porting.service"; import { SecretsManagerPortingService } from "../services/sm-porting.service";
@ -42,7 +42,7 @@ export class SecretsManagerExportComponent implements OnInit, OnDestroy {
private smPortingService: SecretsManagerPortingService, private smPortingService: SecretsManagerPortingService,
private fileDownloadService: FileDownloadService, private fileDownloadService: FileDownloadService,
private logService: LogService, private logService: LogService,
private modalService: ModalService, private dialogService: DialogServiceAbstraction,
private secretsManagerApiService: SecretsManagerPortingApiService private secretsManagerApiService: SecretsManagerPortingApiService
) {} ) {}
@ -98,12 +98,11 @@ export class SecretsManagerExportComponent implements OnInit, OnDestroy {
} }
private verifyUser() { private verifyUser() {
const ref = this.modalService.open(UserVerificationPromptComponent, { const ref = openUserVerificationPrompt(this.dialogService, {
allowMultipleModals: true,
data: { data: {
confirmDescription: "exportWarningDesc", confirmDescription: "exportSecretsWarningDesc",
confirmButtonText: "exportVault", confirmButtonText: "exportSecrets",
modalTitle: "confirmVaultExport", modalTitle: "confirmSecretsExport",
}, },
}); });
@ -111,6 +110,6 @@ export class SecretsManagerExportComponent implements OnInit, OnDestroy {
return; return;
} }
return ref.onClosedPromise(); return firstValueFrom(ref.closed);
} }
} }

View File

@ -2,9 +2,9 @@ import { Directive } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";

View File

@ -2,9 +2,9 @@ import { Directive } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason"; import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";

View File

@ -1,9 +1,10 @@
import { Directive } from "@angular/core"; import { Directive } from "@angular/core";
import { FormBuilder, FormControl } from "@angular/forms"; import { FormBuilder } from "@angular/forms";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Verification } from "@bitwarden/common/types/verification";
import { ModalRef } from "../../components/modal/modal.ref"; import { ModalRef } from "../../components/modal/modal.ref";
import { ModalConfig } from "../../services/modal.service"; import { ModalConfig } from "../../services/modal.service";
@ -16,7 +17,12 @@ export class UserVerificationPromptComponent {
confirmDescription = this.config.data.confirmDescription; confirmDescription = this.config.data.confirmDescription;
confirmButtonText = this.config.data.confirmButtonText; confirmButtonText = this.config.data.confirmButtonText;
modalTitle = this.config.data.modalTitle; modalTitle = this.config.data.modalTitle;
secret = new FormControl();
formGroup = this.formBuilder.group({
secret: this.formBuilder.control<Verification | null>(null),
});
protected invalidSecret = false;
constructor( constructor(
private modalRef: ModalRef, private modalRef: ModalRef,
@ -27,19 +33,31 @@ export class UserVerificationPromptComponent {
private i18nService: I18nService private i18nService: I18nService
) {} ) {}
async submit() { get secret() {
try { return this.formGroup.controls.secret;
//Incorrect secret will throw an invalid password error. }
await this.userVerificationService.verifyUser(this.secret.value);
} catch (e) { submit = async () => {
this.platformUtilsService.showToast( this.formGroup.markAllAsTouched();
"error",
this.i18nService.t("error"), if (this.formGroup.invalid) {
this.i18nService.t("invalidMasterPassword")
);
return; return;
} }
this.modalRef.close(true); try {
//Incorrect secret will throw an invalid password error.
await this.userVerificationService.verifyUser(this.secret.value);
this.invalidSecret = false;
} catch (e) {
this.invalidSecret = true;
this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message);
return;
}
this.close(true);
};
close(success: boolean) {
this.modalRef.close(success);
} }
} }

View File

@ -1,9 +1,11 @@
import { Directive, OnInit } from "@angular/core"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { ControlValueAccessor, FormControl } from "@angular/forms"; import { ControlValueAccessor, FormControl, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { Verification } from "@bitwarden/common/types/verification"; import { Verification } from "@bitwarden/common/types/verification";
@ -17,29 +19,65 @@ import { Verification } from "@bitwarden/common/types/verification";
selector: "app-user-verification", selector: "app-user-verification",
}) })
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class UserVerificationComponent implements ControlValueAccessor, OnInit { export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy {
usesKeyConnector = false; private _invalidSecret = false;
@Input()
get invalidSecret() {
return this._invalidSecret;
}
set invalidSecret(value: boolean) {
this._invalidSecret = value;
this.invalidSecretChange.emit(value);
// ISSUE: This is pretty hacky but unfortunately there is no way of knowing if the parent
// control has been marked as touched, see: https://github.com/angular/angular/issues/10887
// When that functionality has been added we should also look into forwarding reactive form
// controls errors so that we don't need a separate input/output `invalidSecret`.
if (value) {
this.secret.markAsTouched();
}
this.secret.updateValueAndValidity({ emitEvent: false });
}
@Output() invalidSecretChange = new EventEmitter<boolean>();
usesKeyConnector = true;
disableRequestOTP = false; disableRequestOTP = false;
sentCode = false; sentCode = false;
secret = new FormControl(""); secret = new FormControl("", [
Validators.required,
() => {
if (this.invalidSecret) {
return {
invalidSecret: {
message: this.usesKeyConnector
? this.i18nService.t("incorrectCode")
: this.i18nService.t("incorrectPassword"),
},
};
}
},
]);
private onChange: (value: Verification) => void; private onChange: (value: Verification) => void;
private destroy$ = new Subject<void>();
constructor( constructor(
private keyConnectorService: KeyConnectorService, private keyConnectorService: KeyConnectorService,
private userVerificationService: UserVerificationService private userVerificationService: UserVerificationService,
private i18nService: I18nService
) {} ) {}
async ngOnInit() { async ngOnInit() {
this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(); this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
this.processChanges(this.secret.value); this.processChanges(this.secret.value);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil this.secret.valueChanges
this.secret.valueChanges.subscribe((secret: string) => this.processChanges(secret)); .pipe(takeUntil(this.destroy$))
.subscribe((secret: string) => this.processChanges(secret));
} }
async requestOTP() { requestOTP = async () => {
if (this.usesKeyConnector) { if (this.usesKeyConnector) {
this.disableRequestOTP = true; this.disableRequestOTP = true;
try { try {
@ -49,7 +87,7 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit {
this.disableRequestOTP = false; this.disableRequestOTP = false;
} }
} }
} };
writeValue(obj: any): void { writeValue(obj: any): void {
this.secret.setValue(obj); this.secret.setValue(obj);
@ -72,7 +110,14 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit {
} }
} }
private processChanges(secret: string) { ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
protected processChanges(secret: string) {
this.invalidSecret = false;
if (this.onChange == null) { if (this.onChange == null) {
return; return;
} }

View File

@ -18,8 +18,6 @@ import { OrganizationUserService } from "@bitwarden/common/abstractions/organiza
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/abstractions/userVerification/userVerification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.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";
@ -48,6 +46,8 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service"; import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";

View File

@ -3,9 +3,9 @@ import { UntypedFormBuilder, Validators } from "@angular/forms";
import { merge, startWith, Subject, takeUntil } from "rxjs"; import { merge, startWith, Subject, takeUntil } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.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 { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { EncryptedExportType, EventType } from "@bitwarden/common/enums"; import { EncryptedExportType, EventType } from "@bitwarden/common/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";

View File

@ -1,4 +1,4 @@
import { VerifyOTPRequest } from "../../auth/models/request/verify-otp.request"; import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
export abstract class UserVerificationApiServiceAbstraction { export abstract class UserVerificationApiServiceAbstraction {
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>; postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;

View File

@ -1,5 +1,5 @@
import { SecretVerificationRequest } from "../../auth/models/request/secret-verification.request"; import { Verification } from "../../../types/verification";
import { Verification } from "../../types/verification"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
export abstract class UserVerificationService { export abstract class UserVerificationService {
buildRequest: <T extends SecretVerificationRequest>( buildRequest: <T extends SecretVerificationRequest>(

View File

@ -1,9 +1,9 @@
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { UserVerificationService } from "../../abstractions/userVerification/userVerification.service.abstraction";
import { LogService } from "../../platform/abstractions/log.service"; import { LogService } from "../../platform/abstractions/log.service";
import { Verification } from "../../types/verification"; import { Verification } from "../../types/verification";
import { AccountApiService } from "../abstractions/account-api.service"; import { AccountApiService } from "../abstractions/account-api.service";
import { InternalAccountService } from "../abstractions/account.service"; import { InternalAccountService } from "../abstractions/account.service";
import { UserVerificationService } from "../abstractions/user-verification/user-verification.service.abstraction";
export class AccountApiServiceImplementation implements AccountApiService { export class AccountApiServiceImplementation implements AccountApiService {
constructor( constructor(

View File

@ -1,5 +1,5 @@
import { ApiService } from "../../../abstractions/api.service"; import { ApiService } from "../../../abstractions/api.service";
import { UserVerificationApiServiceAbstraction } from "../../../abstractions/userVerification/userVerification-api.service.abstraction"; import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
import { VerifyOTPRequest } from "../../models/request/verify-otp.request"; import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
export class UserVerificationApiService implements UserVerificationApiServiceAbstraction { export class UserVerificationApiService implements UserVerificationApiServiceAbstraction {

View File

@ -1,8 +1,8 @@
import { UserVerificationApiServiceAbstraction } from "../../../abstractions/userVerification/userVerification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../../abstractions/userVerification/userVerification.service.abstraction";
import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service"; import { I18nService } from "../../../platform/abstractions/i18n.service";
import { Verification } from "../../../types/verification"; import { Verification } from "../../../types/verification";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "../../enums/verification-type"; import { VerificationType } from "../../enums/verification-type";
import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { VerifyOTPRequest } from "../../models/request/verify-otp.request"; import { VerifyOTPRequest } from "../../models/request/verify-otp.request";

View File

@ -0,0 +1,67 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./avatar.stories";
<Meta of={stories} />
# Avatar
Avatars display a unique color that helps a user visually recognize their logged in account.
A variance in color across the avatar component is important as it is used in Account Switching as a
visual indicator to recognize which of a personal or work account a user is logged into.
<Primary />
<Controls />
## Size
### Large: 64px
<Story of={stories.Large} />
### Default: 48px
<Story of={stories.Default} />
### Small 28px
<Story of={stories.Small} />
## Background color
The Background color can be set 3 ways. The color is generated using the following order of
priority:
- Color
- ID
- Text, usually set to the user's Name field
<Story of={stories.ColorByText} />
Use the user 'ID' field if `Name` is not defined.
<Story of={stories.ColorByID} />
## Outline
If the avatar is displayed on one of the theme's `background` color variables or is interactive,
display the avatar with a 1 pixel `secondary-500` border to meet WCAG AA graphic contrast guidelines
for interactive elements.
<Story of={stories.Border} />
## Avatar as a button
The Avatar can be used as a button.
Typically this is only in the navigation on client apps where account switching is used and in the
web app for the account menu indicator.
When the avatar is used as a button, the following states should be used:
`TODO:` [Jira add stories](https://bitwarden.atlassian.net/browse/CL-101) for button avatars.
[See Figma](https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?type=design&node-id=9730-31746&mode=design&t=IjDIHDb6FZl6bUQW-4)
## Accessibility
Avatar background color should have 3.1:1 contrast with its background; or include the
`secondary-500` border Avatar text should have 4.5:1 contrast with the avatar background color

View File

@ -0,0 +1,67 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./badge.stories";
<Meta of={stories} />
# Badge
The Badge directive can be used on a `<span>` (non clickable events), or an `<a>` or `<button>` tag
for interactive events. The Focus and Hover states only apply to badges used for interactive events.
Typically Badges are only used with text set to `text-xs`. If additional sizes are needed, the
component configurations may be reviewed and adjusted.
<Primary />
<Controls />
## Styles
### Primary
The primary badge is used to indicate an active status (example: device management page) or provide
additional information (example: type of emergency access granted).
<Story of={stories.Primary} />
### Secondary
The secondary badge style is typically is a default badge style. It is often used to indicate
neutral information.
<Story of={stories.Secondary} />
### Success
The success badge is used to indicate a positive status, OR to indicate a feature requires a Premium
subscription. See [Premium Badge](?path=/docs/web-premium-badge--docs)
<Story of={stories.Success} />
### Danger
The danger badge is used to indicate a negative status.
<Story of={stories.Danger} />
### Warning
The warning badge is used to indicate a status waiting on an additional action from the active user.
<Story of={stories.Warning} />
### Info
The info badge is used to indicate a low emphasis informative information.
<Story of={stories.Info} />
## Badges as counters
Badges can be used as part of links or buttons to provide a counter. See the
[Toggle Group](?path=/docs/component-library-toggle-group--docs) component.
## Accessibility
Be sure to use the correct html tag based on the purpose or function of the badge. Follow color WCAG
color contrast guidelines for small text.

View File

@ -10,7 +10,7 @@ Banners are used for important communication with the user that needs to be seen
little effect on the experience. Banners appear at the top of the user's screen on page load and little effect on the experience. Banners appear at the top of the user's screen on page load and
persist across all pages a user navigates to. persist across all pages a user navigates to.
- They should always be dismissable and never use a timeout. If a user dismisses a banner, it should - They should always be dismissible and never use a timeout. If a user dismisses a banner, it should
not reappear during that same active session. not reappear during that same active session.
- Use banners sparingly, as they can feel intrusive to the user if they appear unexpectedly. Their - Use banners sparingly, as they can feel intrusive to the user if they appear unexpectedly. Their
effectiveness may decrease if too many are used. effectiveness may decrease if too many are used.

View File

@ -0,0 +1,54 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./breadcrumbs.stories";
<Meta of={stories} />
# Breadcrumbs
Breadcrumbs are used to help users understand where they are in a products navigation. Typically
Bitwarden uses this component to indicate the user's current location in a set of data organized in
containers (Collections, Folders, or Projects).
<Primary />
<Controls />
## Display
Breadcrumbs display above the page title. The current page should not appear as a breadcrumb link.
See [Header with Breadcrumbs](?path=/story/web-header--with-breadcrumbs).
### Top Level
When a user is 1 level deep into a tree, the top level is displayed as a single link above the page
title.
<Story of={stories.TopLevel} />
### Second Level
When a user is 2 or more levels deep into a tree, the top level is displayed followed by an
<i class="bwi bwi-angle-right"></i> icon, and the following pages.
<Story of={stories.SecondLevel} />
### Overflow
When a user is several levels deep into a tree, the top level or 2 are displayed followed by an
<i class="bwi bwi-ellipsis-h"> </i> icon button, and then the page directly above the current page.
When the user selects the <i class="bwi bwi-ellipsis-h"></i> icon button, a menu opens displaying
the pages between the top level and the previous page.
<Story of={stories.Overflow} />
### Small screens
If a screen's width is not large enough to display the full breadcrumb path, display a link to the
previous page and an <i class="bwi bwi-angle-right"></i> icon to take the user back to the previous
page.
`TODO:` [Jira add stories](https://bitwarden.atlassian.net/browse/CL-102) for responsive screen
width/small screens

View File

@ -33,31 +33,6 @@ Groups within page content, dialog footers or forms should have the `primary` ca
to left. Groups in headers and navigational areas should have the `primary` call to action on the to left. Groups in headers and navigational areas should have the `primary` call to action on the
right. right.
## Accessibility
Please follow these guidelines to ensure that buttons are accessible to all users.
### Color contrast
All button styles are WCAG compliant when displayed on `background` and `background-alt` colors. To
use a button on a different background, double check that the color contrast is sufficient in both
the light and dark themes.
### Loading Buttons
Include an `aria-label` attribute that defaults to “loading” but can be configurable per
implementation. On click, the screen reader should announce the `aria-label`. Once the action is
compelted, use another messaging pattern to alert the user that the action is complete (example:
success toast).
### Submit and async actions
Both submit and async action buttons use a loading button state while an action is taken. If your
button is preforming a long running task in the background like a server API call, be sure to review
the [Async Actions Directive](?path=/story/component-library-async-actions-overview--page).
<Story of={stories.Loading} />
## Styles ## Styles
There are 3 main styles for the button: Primary, Secondary, and Danger. There are 3 main styles for the button: Primary, Secondary, and Danger.
@ -96,3 +71,39 @@ Typically button widths expand with their text. In some causes though buttons ma
where the width is fixed and the text wraps to 2 lines if exceeding the buttons width. where the width is fixed and the text wraps to 2 lines if exceeding the buttons width.
<Story of={stories.Block} /> <Story of={stories.Block} />
## Accessibility
Please follow these guidelines to ensure that buttons are accessible to all users.
### Color contrast
All button styles are WCAG compliant when displayed on `background` and `background-alt` colors. To
use a button on a different background, double check that the color contrast is sufficient in both
the light and dark themes.
### Loading Buttons
Include an `aria-label` attribute that defaults to "loading" but can be configurable per
implementation. On click, the screen reader should announce the `aria-label`. Once the action is
completed, use another messaging pattern to alert the user that the action is complete (example:
success toast).
### Submit and async actions
Both submit and async action buttons use a loading button state while an action is taken. If your
button is preforming a long running task in the background like a server API call, be sure to review
the [Async Actions Directive](?path=/story/component-library-async-actions-overview--page).
<Story of={stories.Loading} />
### appA11yTitle
`appA11yTitle` is a directive that auto assigns the same string to the `title` and `aria-label`
attributes.
When a button uses accessible content (e.i. actual text), DO NOT include this as it adds redundant
content for someone using assistive technology.
`appA11yTitle` should only be used if the element it applies to does not include accessible text,
e.i. an icon.

View File

@ -0,0 +1,66 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./callout.stories";
<Meta of={stories} />
# Callouts
Callouts are used to communicate important information to the user. Callouts should be used
sparingly, as they command a large amount of visual attention. Avoid using more than 1 callout in
the same location.
## Styles
Icons should remain consistent across these types. Do not change the icon without consulting a
designer. Use the following guidelines to help choose the correct type of callout.
### Success
Use the success callout to communicate a positive messaging to the user.
**Example:** a positive report results shows a success callout.
The success callout may also be used for the information related to a premium membership. In this
case, replace the icon with <i class="bwi bwi-star" title="bwi-star" aria-label="bwi-star"></i>
<Story of={stories.Success} />
### Info
Use an info callout to call attention to important information the user should be aware of, but has
low risk of the user receiving and unintended or irreversible results if they do not read the
information.
**Example:** in the Domain Claiming modal, an info callout is used to tell the user the domain will
automatically be checked.
<Story of={stories.Info} />
### Warning
Use a warning callout if the user is about to perform an action that may have unintended or
irreversible results.
**Example:** the warning callout is used before the change master password and encryption key form
to alert the user that they will be logged out.
<Story of={stories.Warning} />
### Danger
Use the danger callout to communicate an action the user is about to take is dangerous and typically
not reversible.
The danger callout can also be used to alert the user of an error or errors, such as a server side
errors after form submit or failed communication request.
<Story of={stories.Danger} />
## Accessibility
Use the `role=”alert”` only if the callout is appearing on a page after the user takes an action. If
the content is static, do not use the alert role. This will cause a screen reader to announce the
callout content on page load.
Ensure the title's color contrast remains WCAG compliant with the callout's background.

View File

@ -0,0 +1,34 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./color-password.stories";
<Meta of={stories} />
# Color password
The color password is used primarily in the Generator pages and in the Login type form. It includes
the logic for displaying letters as `text-main`, numbers as `primary`, and special symbols as
`danger`.
<Primary />
<Controls />
## Password Count
The password count option is used in the Login type form. It is used to highlight each character's
position in the password string.
<Story of={stories.ColorPasswordCount} />
## Wrapped Password
When the password length is longer than the container's width, it should wrap as shown below.
<Story of={stories.WrappedColorPassword} />
<Story of={stories.WrappedColorPasswordCount} />
## Accessibility
The colors used in the colored password should maintain WCAG compliant contrast with theme
`background` and `background-alt` colors.

View File

@ -0,0 +1,73 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./dialog.stories";
<Meta of={stories} />
# Dialog
Dialogs are used throughout the app to help the user focus on a specific action. Use this dialog
component when content exceeds 384px width or there are a high number of interactive elements
needed. **Example:** The web app's edit vault item form dialog
For alerts or simple confirmation actions, like speedbumps, use the
[Simple Dialog](?path=/docs/component-library-dialogs-simple-dialog--docs).
Dialogs's should be used sparingly as they do call extra attention to themselves and can be
interruptive if overused.
<Primary />
<Controls />
## Size
There are 3 main dialog sizes:
### Large
Use the large size for dialogs that have many interactive elements or tabbed content.
**Tailwind styling:**
`max-w-3xl` 48rem
<Story of={stories.Large} />
### Default
Use the Default size for most dialogs that require some content and a few interactive elements.
**Example:** master password confirmation dialog
**Tailwind styling:**
`max-w-xl` 36rem
<Story of={stories.Default} />
### Small
**Tailwind styling:**
`max-w-sm` 24rem
<Story of={stories.Small} />
## Long Title
If a dialog's title is too long to fully display. It should be truncated and on hover shown in a
tooltip.
<Story of={stories.LongTitle} />
## Loading
Similar to a page loading state, a Dialog that takes more than a few seconds to load should use a
loading state.
<Story of={stories.Loading} />
## Tab Content
Use tabs to separate related content within a dialog.
<Story of={stories.TabContent} />

View File

@ -0,0 +1,56 @@
import { Meta, Story, Source } from "@storybook/addon-docs";
<Meta title="Component Library/Dialogs" />
# Dialog
Dialogs are used throughout the app to help the user focus on a specific action.
Use the main [Dialog Component](?path=/docs/component-library-dialogs-dialog--docs). when content
exceeds 384px width or there are a high number of interactive elements needed. **Example:** The web
app's edit vault item form dialog
For alerts or simple confirmation actions, like speedbumps, use the
[Simple Dialog](?path=/docs/component-library-dialogs-simple-dialog--docs).
Dialogs's should be used sparingly as they do call extra attention to themselves and can be
interruptive if overused.
## Placement
Dialogs should be centered vertically and horizontally on screen. Dialogs height should expand to
fit its content until there are 2rems of margin on the top/bottom of the dialog; in this case, the
dialog should become scrollable.
A backdrop should be used to hide the content below the dialog. Use `#000000` with `30% opacity`.
<Story id="component-library-dialogs-service--default" />
## Accessibility
### Component behavior
- Dialog include `role="dialog"`
- The Dialog title is an `<h1>`
- A user should not be able to tab focus outside of the Dialog until it has been closed.
- Clicking outside the dialog or clicking escape should close the dialog (this prevents a keyboard
trap)
### Required per implementation
The triggering button should indicate to assistive technology that additional content will open or
appear when the trigger is selected. Consider using `aria-haspopup="true"`
Dialog title should be announced by a screen reader when launched. Consider using `aria-labelledby`
or `aria-label`
When opened, focus should follow the visual order of the popovers focusable content. Typically
focus is moved to the close button, but it is acceptable to move focus to the first interactive
element after close since a user may not want to close the dialog immediately if there are
additional interactive elements. See
[WCAG Focus Order success criteria](https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html)
Once closed, focus should remain on the the element which triggered the Dialog.
**Note:** If a Simple Dialog is triggered from a main Dialog, be sure to make sure focus is moved to
the Simple Dialog.

View File

@ -0,0 +1,47 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./simple-dialog.stories";
<Meta of={stories} />
# Simple Dialogs
Simple Dialogs are used throughout the app for simple alert or confirmation actions such as
speedbumps.
For dialogs with a high number of interactive elements such as a form or content exceeding 384px,
use the [Dialog component](?path=/docs/component-library-dialogs-dialog--docs).
<Primary />
<Controls />
## Configurable Simple Dialog
The Simple Dialog contains the following configuration points:
- `title`: string
- `content`: string
- `type`: SimpleDialogType
- `icon`: string if empty, infer from type
- `acceptButtonText`: string if empty, default to "Yes"
- `cancelButtonText`: string if empty, default to "No", unless acceptButtonText is overridden, in
which case default to "Cancel"
To increase consistency, the simple dialog service supports some automation for setting the `icon`
and `color` based on the defined type. See the following for how properties will be configured when
the simple dialog's type is specified.
| type | icon name | icon | color |
| ------- | ------------------------ | -------------------------------------------- | ----------- |
| primary | bwi-business | <i class="bwi bwi-business"></i> | primary-500 |
| success | bwi-star | <i class="bwi bwi-star"></i> | success-500 |
| info | bwi-info-circle | <i class="bwi bwi-info-circle"></i> | info-500 |
| warning | bwi-exclamation-triangle | <i class="bwi bwi-exclamation-triangle"></i> | warning-500 |
| danger | bwi-error | <i class="bwi bwi-error"></i> | danger-500 |
## Scrolling Content
Simple dialogs can support scrolling content if necessary, but typically with larger quantities of
content a [Dialog component](?path=/docs/component-library-dialogs-dialog--docs).
<Story of={stories.ScrollingContent} />

View File

@ -1,8 +1,11 @@
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { import {
AfterContentChecked, AfterContentChecked,
Component, Component,
ContentChild, ContentChild,
ContentChildren, ContentChildren,
HostBinding,
Input,
QueryList, QueryList,
ViewChild, ViewChild,
} from "@angular/core"; } from "@angular/core";
@ -17,9 +20,6 @@ import { BitSuffixDirective } from "./suffix.directive";
@Component({ @Component({
selector: "bit-form-field", selector: "bit-form-field",
templateUrl: "./form-field.component.html", templateUrl: "./form-field.component.html",
host: {
class: "tw-mb-6 tw-block",
},
}) })
export class BitFormFieldComponent implements AfterContentChecked { export class BitFormFieldComponent implements AfterContentChecked {
@ContentChild(BitFormFieldControl) input: BitFormFieldControl; @ContentChild(BitFormFieldControl) input: BitFormFieldControl;
@ -30,6 +30,19 @@ export class BitFormFieldComponent implements AfterContentChecked {
@ContentChildren(BitPrefixDirective) prefixChildren: QueryList<BitPrefixDirective>; @ContentChildren(BitPrefixDirective) prefixChildren: QueryList<BitPrefixDirective>;
@ContentChildren(BitSuffixDirective) suffixChildren: QueryList<BitSuffixDirective>; @ContentChildren(BitSuffixDirective) suffixChildren: QueryList<BitSuffixDirective>;
private _disableMargin = false;
@Input() set disableMargin(value: boolean | "") {
this._disableMargin = coerceBooleanProperty(value);
}
get disableMargin() {
return this._disableMargin;
}
@HostBinding("class")
get classList() {
return ["tw-block"].concat(this.disableMargin ? [] : ["tw-mb-6"]);
}
ngAfterContentChecked(): void { ngAfterContentChecked(): void {
if (this.error) { if (this.error) {
this.input.ariaDescribedBy = this.error.id; this.input.ariaDescribedBy = this.error.id;

View File

@ -0,0 +1,178 @@
import { Meta, Story, Source } from "@storybook/addon-docs";
<Meta title="Component Library/Form" />
# Forms
Component Library forms should always be built using [Angular Reactive Forms][reactive]. Please read
[ADR-0001][adr-0001] for a background to this decision. In practice this means that forms should
always use the native `form` element and bind a `formGroup`.
<Story id="component-library-form--full-example" />
<Source id="component-library-form--full-example" />
## Form spacing and sections
Forms consists of 1 or more inputs, and ends with 1 or 2 buttons.
If there are many inputs in a form, they should should be organized into sections as content
relates. **Example:** Item type form
Each input within a section should follow the following spacing guidelines (see
[Tailwind CSS spacing documentation](https://tailwindcss.com/docs/customizing-spacing)):
- 1.5rem of vertical spacing between form elements: `mb-6`
- 1.5rem of horizontal spacing between form elements: `mr-6`
- 3rem of vertical spacing below a form section: `mb-12`
- 1rem of vertical spacing between a form group divider and the group's title; so title tag has:
`my-4`
- Form section titles should be styled using `text-lg`
- Each form sections may have a single column, double or triple column layout. No form should have
more than 3 columns. Do NOT use different column layouts within the same form section. Choose the
best layout based on the number of fields and type of fields included.
## Input Types
### Field
A form field is the most common input in a form. It consists of a label, control and an optional
hint.
The styling of form fields applies to all field types: `text`, `number`, `select`, `text-area`,
`date`, etc.
Be sure to use an appropriate type attribute on fields when defining new field components (e.g.
`email` for email address or `number` for numerical information) to take advantage of newer input
controls like email verification, number selection, and more.
#### Default with required attribute
<Story id="component-library-form-field--default" />
#### Password Toggle
<Story id="component-library-form-password-toggle--default" />
#### Search
<Story id="component-library-form-search--default" />
### Selects
#### Searchable single select (default)
<Story id="component-library-form-select--default" />
#### Multi-select
<Story id="component-library-form-multi-select--members" />
### Radio group
Radio buttons should always be in radio groups.
Radio groups are form fields that consists of a main label and multiple radio buttons. Each radio
button consists of a label and a radio input.
The full form control + label should be selectable to allow the user a larger click target.
Radio groups should always have a default selected value.
Radio groups may optionally include extra helper text below each radio button.
If a radio group has more than 4 options and the options do not need helper text, a
[select menu](?path=/docs/component-library-form-multi-select--docs) should be used instead. Avoid
using a radio group for more than 5 options even if the options require additional explanation text.
`TODO: extend the select component to support a dropdown menu with descriptions below each option`
#### Block
<Story id="component-library-form-radio-button--block" />
#### Inline
<Story id="component-library-form-radio-button--inline" />
[reactive]: https://angular.io/guide/reactive-forms
[adr-0001]: https://contributing.bitwarden.com/architecture/adr/reactive-forms
### Checkbox
The checkbox input is used to toggle an action on/off.
Checkboxes can be displayed on their own or in a group (select multiple form question). When
displayed in a group, include an input Label and any associated required/validation logic for the
field.
Unlike radio groups, checkbox groups are not required to have a default selected value.
Checkbox groups can include extra explanation text below each radio button or just the checkbox
button itself.
If a checkbox group has more than 4 options a
[multi-select components](?path=/docs/component-library-form-multi-select--docs) should be used.
#### Single checkbox
<Story id="component-library-form-checkbox--default" />
## Accessibility
### Required Fields
- Use "(required)" in the label of each required form field styled the same as the field's helper
text (`.muted-text`).
- If whether or not a form field is required depends on another field, add this to the field's
helper text.
- **Example:** "Billing Email is required if owned by a business".
### Form Field Errors
- When a resting field is filled out, validation is triggered when the user de-focuses the field
(`onblur`). If the control is invalid, assistive technology should announce the error (consider
using `role="alert"` or an `aria-live="assertive"`).
- Validation should not be triggered if the control is left untouched; this allows a user of
assistive technology to read the entire form if they wish without triggering validation that could
interrupt them. - **TODO:** research how we might implement this behavior; as previous research
has shown Angular may not allow both validation when `dirty` `onblur` AND validation on Submit
which is a requirement
- A form control with an error should change to the error UI and the error text should be displayed
below the element and be associated to their respective fields (consider using `aria-describedby`)
- When a field with an error is focused, assistive technology should announce the label and
elements' invalid state and then the error text.
- **Example:** "URL required, Error, URL format is not acceptable."
- Once the user has re-focused the field, and starts typing. The error will disappear. Validation
should not occur when typing in most cases. Once th user unfocuses the field, validation triggers
again.
### Validation on Submit
- Validation must also occur on submit. A user may select the submit button directly without
changing focus from a form input. Or a user may disable their browser's javascript which is what
supports the inline onblur validation. Finally, there may be a server side error that can only be
checked on submit.
- On submit, a summary error should appear near the submit button or at the top of the form alerting
the user of what errors need to be addressed. This summary should be read out by assistive
technology after submit regardless of whether or not it was already on screen.
- Any invalid form control will display an inline error following the field's helper text (or in
place of)
- If submit is successful, use a success toast to alert the user of the successful action.
- For any server side errors, the Danger toast may still be used. Be sure to adjust the toast's
timeout to follow the 6 second
* 1 second for each additional 120 words rule.
### Helper Text
Similar to a field error, helper text should be associated to a field using `aria-describedby`. This
allows assistive technology to read out the instructional text and field requirements in addition to
the fields label.
### Visual style
- All field inputs are interactive elements that must follow the WCAG graphic contrast guidelines.
Maintain a ratio of 3:1 with the form's background.
- Error styling should not rely only on using the `danger-500`color change. Use
<i class="bwi bwi-error"></i> as a prefix to highlight the text as error text versus helper

View File

@ -0,0 +1,55 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./icon-button.stories";
<Meta of={stories} />
# Icon Button
Icon buttons are used when no text accompanies the button. It consists of an icon that may be
updated to any icon in the `bwi-font`, a `title` attribute, and an `aria-label`.
There are 3 common styles for button main, contrast, and danger. The main style is used on the
themes main `background`; and the contrast style is used on a themes colored or contrasting
backgrounds; danger is used for “trash” actions throughout the experience. The other styles are used
sparingly.
The most common use of the icon button is in the banner, toast, and modal components as a close
button. It can also be found in tables as the 3 dot option menu, or on navigation list items when
there are options that need to be collapsed into a menu.
Similar to the main button components, spacing between icon buttons should be .5rem.
<Primary />
<Controls />
**Note:** Main and contrast styles appear on backgrounds where using `primary-700` as a focus
indicator does not meet WCAG graphic contrast guidelines.
## Sizes
There are 2 sizes for the icon button: `small` and `default`.
Default is typically used for most instances. Small is used if the implementation needs a variant
with less padding around the icon, such as in the navigation component.
## Usage
Icon buttons can be found in other components such as: the
[banner](?path=/docs/component-library-banner--docs)
[dialog](?path=/docs/component-library-dialogs--docs), and
[table](?path=/docs/component-library-table--docs).
<Story id="component-library-banner--premium" />
## Accessibility
Follow guidelines outlined in the [Button docs](?path=/docs/component-library-button--doc)
Always use the `appA11yTitle` directive set to a string that describes the action of the
icon-button. This will auto assign the same string to the `title` and `aria-label` attributes.
`aria-label` allows assistive technology to announce the action the button takes to the users.
`title` attribute provides a user with the browser tool tip if they do not understand what the icon
is indicating.

View File

@ -1,6 +1,6 @@
import { Meta } from "@storybook/addon-docs"; import { Meta } from "@storybook/addon-docs";
<Meta title="Component Library/Form/Input" /> <Meta title="Component Library/Form/Input Directive" />
# Input # Input

View File

@ -0,0 +1,39 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./link.stories";
<Meta of={stories} />
# Link / Text button
Text Links and Buttons use the `primary-500` color and can use either the `<a>` or `<button>` tags.
Choose which based on the action the button takes:
- if navigating to a new page, use a `<a>`
- if taking an action on the current page use a `<button>`
Text buttons or links are most commonly used in paragraphs of text or in forms to customize actions
or show/hide additional form options.
<Primary />
<Controls />
## Sizes
There are 2 sizes for the component: default and small.
Default uses `text-base` and small uses `text-xs`
## With icons
Text Links/buttons can have icons on left or the right.
To indicate a new or add action, the <i class="bwi bwi-plus-circle"></i> icon on is used on the
left.
An angle icon, <i class="bwi bwi-angle-right"></i>, is used on the left to indicate an expand to
show/hide additional content.
## Accessibility
Make sure to only use the Link on backgrounds that maintain the WCAG color contrast ratios.

View File

@ -0,0 +1,21 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./menu.stories";
<Meta of={stories} />
# Menu
Menus are used to help organize related options. Menus are most often used for item options in
tables.
<Story of={stories.ClosedMenu} />
<Controls />
## Accessibility
Follow WCAG AA best practices. Example: Insure the triggering element has `aria-haspopup="true"`
prior to being clicked and `aria-expanded="true"` after the user clicks the element.
User should be able to navigate the opened menu via the up and down arrow keys and close the menu
using the escape key or clicking out of the menu.

View File

@ -0,0 +1,48 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./progress.stories";
<Meta of={stories} />
# Progress
Progress indicators may be used to visually indicate progress or to visually measure some other
value, such as a password strength indicator.
<Primary />
<Controls />
## Labels
Always display a label to provide a text based description of what the indicator is measuring. This
allows those who may not be familiar with the pattern to be able to read and digest the information.
It also allows assistive technology to accurately describe the indicator to those who may be unable
to see part or all of the indicator.
<Story of={stories.Full} />
## Text label
When measuring something other than progress, such as password strength, update the label to fit the
context of the implementation.
<Story of={stories.CustomText} />
### Strength indicator styles
For a strength indicator use the following styles for fill:
- **Weak:** `danger-500`
- **Weak2:** `warning-500`
- **Good:** `primary-500`
- **Strong:** `success-500`
## Accessibility
Be sure to include the proper `aria-valuemin`, `aria-valuemax`, and `aria-valuenow` attributes. An
a`ria-valuetext` should also be configurable to include the text a screen reader should read to the
user.
In the case of a password strength indicator; `aria-describedby` is used on the password field and
points to the `id` of the progress bar. This results in the screen reader reading the password
strength to the user after they finish typing.

View File

@ -1,51 +0,0 @@
import { Meta, Story, Source } from "@storybook/addon-docs";
<Meta title="Component Library/Form" />
# Forms
Examples and usage guidelines for form control styles, layout options, and custom components for
creating a wide variety of forms.
## Overview
Component Library forms should always be built using [Angular Reactive Forms][reactive]. Please read
[ADR-0001][adr-0001] for a background to this decision. In practice this means that forms should
always use the native `form` element and bind a `formGroup`.
Forms consists of one or multiple sections, and ends with one or multiple buttons.
### Form Field
A form field is the most common section in a form. It consists of a label, control and a optional
hint.
<Story id="component-library-form-field--default" />
<Source id="component-library-form-field--default" />
### Radio group
A radio group is a form field that consists of a main label and multiple radio groups. Each group
consists of a label and a radio input.
#### Block
<Story id="component-library-form-radio-button--block" />
<Source id="component-library-form-radio-button--block" />
#### Inline
<Story id="component-library-form-radio-button--inline" />
<Source id="component-library-form-radio-button--inline" />
## Full Example
<Story id="component-library-form--full-example" />
<Source id="component-library-form--full-example" />
[reactive]: https://angular.io/guide/reactive-forms
[adr-0001]: https://contributing.bitwarden.com/architecture/adr/reactive-forms

View File

@ -26,12 +26,18 @@ The UI component consists of a couple of elements.
### Guidelines ### Guidelines
- Always include a row or column header with your table; this allows screen readers to better - Always include a row or column header with your table; this allows screen readers to better
contextualize the data contextualize the data.
- Avoid spanning data across cells. - Avoid spanning data across cells.
- Be sure to make repeating actions unique by associating them with the object they relate to. - Be sure to make repeating actions unique by associating them with the object they relate to.
Example: if there are multiple “Edit” buttons on a table, a screen reader should read “Edit, Example: if there are multiple “Edit” buttons on a table, a screen reader should read “Edit,
Netflix” for an edit option for a Netflix item. Netflix” for an edit option for a Netflix item.
- Use [Virtual Scrolling](#virtual-scrolling) for large data sets. - Use [Virtual Scrolling](#virtual-scrolling) for large data sets.
- For bulk menu options, display a 3 dot menu in the header. When multiple items are selected, the
bulk menu will contain actions that can be completed in bulk for the selected items.
- Note, this may result in some menu actions being hidden at times if they are not applicable to
the selected item
- Clicking on a rows 3 dot menu will continue to result in actions specific to just that row's
item
### Usage ### Usage
@ -147,3 +153,12 @@ specify a `itemSize`, set `scrollWindow` to `true` and replace `*ngFor` with `*c
</bit-table> </bit-table>
</cdk-virtual-scroll-viewport> </cdk-virtual-scroll-viewport>
``` ```
## Accessibility
- Always include a row or column header with your table; this allows assistive technology to better
contextualize the data
- Avoid spanning data across cells
- Be sure to make repeating actions unique by associating them with the object they relate to
- **Example:** if there are multiple “Edit” buttons on a table, a screen reader should read “Edit,
Netflix” for an edit option for a Netflix item.

View File

@ -0,0 +1,40 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./tabs.stories";
<Meta of={stories} />
# Tabs
The tab navigation and content tabs share the same styling. The tab navigation uses links to
navigate between pages, whereas the tab list uses `<buttons>` to toggle content on a single page.
Tabs should be displayed on the `background-alt` color, with their content area set to background
and 1rem of padding on the left and right.
<Primary />
<Controls />
## Content Tabs
<Story of={stories.ContentTabs} />
## Navigation Tabs
<Story of={stories.NavigationTabs} />
## Content tabs in dialogs
Tabs can be used in dialogs to separate related content.
<Story id="component-library-dialogs-dialog--tab-content" />
## Accessibility
**Navigation tabs** are implemented using the `<nav>` element and `<a>` for each tab.
**Content tabs** should be implemented with the `tablist` role and:
- Use `<button>` for the tab elements
- Set `aria-selected` for each tab; “true” for selected and “false” for unselected
- See WCAG for more: https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html

View File

@ -0,0 +1,36 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./toggle-group.stories";
<Meta of={stories} />
# Toggle Group
Toggle groups are used for quick filters for a data set. **Example:** the Members page of the Admin
Console uses a toggle group to filter members by their organization status: all, invited, needs
confirmation, revoked.
Toggle groups function as radio buttons and a radio group under the hood.
A button in a toggle group can have a badge counter added to show the number of items existing
within that filter.
For focus states, use `focus-visible`.
<Primary />
<Controls />
## Accessibility
- Follow contrast rules for the main button styles.
- Focus:
- Implement as a radio group with button styling and a context label (context label can be screen
reader only depending on use case).
- Since only 1 button can be selected at a time to filter the toggle group acts similarly to a
radio group.
- When moving focus to a button group, the focus should always move to the selected button. The
screen reader should then announce the button group: example “[context label], [button content]
selected, of [# of buttons]”), the number of buttons and the currently selected button. The user
may navigate the options then via left/right arrow keys.
See WCAG for more: https://www.w3.org/WAI/ARIA/apg/patterns/radio/