diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index c9a25670a90..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,29 +0,0 @@ -**/build -**/dist -**/coverage -.angular -storybook-static - -**/node_modules - -**/webpack.*.js -**/jest.config.js - -apps/browser/config/config.js -apps/browser/src/auth/scripts/duo.js -apps/browser/webpack/manifest.js - -apps/desktop/desktop_native -apps/desktop/src/auth/scripts/duo.js - -apps/web/config.js -apps/web/scripts/*.js -apps/web/tailwind.config.js - -apps/cli/config/config.js - -tailwind.config.js -libs/components/tailwind.config.base.js -libs/components/tailwind.config.js - -scripts/*.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 3fd6dec3d7e..00000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,258 +0,0 @@ -{ - "root": true, - "env": { - "browser": true, - "webextensions": true - }, - "overrides": [ - { - "files": ["*.ts", "*.js"], - "plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": ["./tsconfig.eslint.json"], - "sourceType": "module", - "ecmaVersion": 2020 - }, - "extends": [ - "eslint:recommended", - "plugin:@angular-eslint/recommended", - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - "plugin:rxjs/recommended", - "prettier", - "plugin:storybook/recommended" - ], - "settings": { - "import/parsers": { - "@typescript-eslint/parser": [".ts"] - }, - "import/resolver": { - "typescript": { - "alwaysTryTypes": true - } - } - }, - "rules": { - "@angular-eslint/component-class-suffix": 0, - "@angular-eslint/contextual-lifecycle": 0, - "@angular-eslint/directive-class-suffix": 0, - "@angular-eslint/no-empty-lifecycle-method": 0, - "@angular-eslint/no-host-metadata-property": 0, - "@angular-eslint/no-input-rename": 0, - "@angular-eslint/no-inputs-metadata-property": 0, - "@angular-eslint/no-output-native": 0, - "@angular-eslint/no-output-on-prefix": 0, - "@angular-eslint/no-output-rename": 0, - "@angular-eslint/no-outputs-metadata-property": 0, - "@angular-eslint/use-lifecycle-interface": "error", - "@angular-eslint/use-pipe-transform-interface": 0, - "@typescript-eslint/explicit-member-accessibility": [ - "error", - { "accessibility": "no-public" } - ], - "@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled - "@typescript-eslint/no-floating-promises": "error", - "@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }], - "@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }], - "@typescript-eslint/no-unused-expressions": ["error", { "allowTernary": true }], - "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], - "no-console": "error", - "import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package. - "import/order": [ - "error", - { - "alphabetize": { - "order": "asc" - }, - "newlines-between": "always", - "pathGroups": [ - { - "pattern": "@bitwarden/**", - "group": "external", - "position": "after" - }, - { - "pattern": "src/**/*", - "group": "parent", - "position": "before" - } - ], - "pathGroupsExcludedImportTypes": ["builtin"] - } - ], - "rxjs-angular/prefer-takeuntil": ["error", { "alias": ["takeUntilDestroyed"] }], - "rxjs/no-exposed-subjects": ["error", { "allowProtected": true }], - "no-restricted-syntax": [ - "error", - { - "message": "Calling `svgIcon` directly is not allowed", - "selector": "CallExpression[callee.name='svgIcon']" - }, - { - "message": "Accessing FormGroup using `get` is not allowed, use `.value` instead", - "selector": "ChainExpression[expression.object.callee.property.name='get'][expression.property.name='value']" - } - ], - "curly": ["error", "all"], - "import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway - "import/no-restricted-paths": [ - "error", - { - "zones": [ - { - "target": ["libs/**/*"], - "from": ["apps/**/*"], - "message": "Libs should not import app-specific code." - }, - { - // avoid specific frameworks or large dependencies in common - "target": "./libs/common/**/*", - "from": [ - // Angular - "./libs/angular/**/*", - "./node_modules/@angular*/**/*", - - // Node - "./libs/node/**/*", - - //Generator - "./libs/tools/generator/components/**/*", - "./libs/tools/generator/core/**/*", - "./libs/tools/generator/extensions/**/*", - - // Import/export - "./libs/importer/**/*", - "./libs/tools/export/vault-export/vault-export-core/**/*" - ] - }, - { - // avoid import of unexported state objects - "target": [ - "!(libs)/**/*", - "libs/!(common)/**/*", - "libs/common/!(src)/**/*", - "libs/common/src/!(platform)/**/*", - "libs/common/src/platform/!(state)/**/*" - ], - "from": ["./libs/common/src/platform/state/**/*"], - // allow module index import - "except": ["**/state/index.ts"] - } - ] - } - ] - } - }, - { - "files": ["*.html"], - "parser": "@angular-eslint/template-parser", - "plugins": ["@angular-eslint/template", "tailwindcss"], - "rules": { - "@angular-eslint/template/button-has-type": "error", - "tailwindcss/no-custom-classname": [ - "error", - { - // uses negative lookahead to whitelist any class that doesn't start with "tw-" - // in other words: classnames that start with tw- must be valid TailwindCSS classes - "whitelist": ["(?!(tw)\\-).*"] - } - ], - "tailwindcss/enforces-negative-arbitrary-values": "error", - "tailwindcss/enforces-shorthand": "error", - "tailwindcss/no-contradicting-classname": "error" - } - }, - { - "files": ["apps/browser/src/**/*.ts", "libs/**/*.ts"], - "excludedFiles": [ - "apps/browser/src/autofill/{content,notification}/**/*.ts", - "apps/browser/src/**/background/**/*.ts", // It's okay to have long lived listeners in the background - "apps/browser/src/platform/background.ts" - ], - "rules": { - "no-restricted-syntax": [ - "error", - { - "message": "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead", - // This selector covers events like chrome.storage.onChange & chrome.runtime.onMessage - "selector": "CallExpression > [object.object.object.name='chrome'][property.name='addListener']" - }, - { - "message": "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead", - // This selector covers events like chrome.storage.local.onChange - "selector": "CallExpression > [object.object.object.object.name='chrome'][property.name='addListener']" - } - ] - } - }, - { - "files": ["**/*.ts"], - "excludedFiles": ["**/platform/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { - "patterns": [ - "**/platform/**/internal", // General internal pattern - // All features that have been converted to barrel files - "**/platform/messaging/**" - ] - } - ] - } - }, - { - "files": ["**/src/**/*.ts"], - "excludedFiles": ["**/platform/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { - "patterns": [ - "**/platform/**/internal", // General internal pattern - // All features that have been converted to barrel files - "**/platform/messaging/**", - "**/src/**/*" // Prevent relative imports across libs. - ] - } - ] - } - }, - { - "files": ["bitwarden_license/bit-common/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/bit-common/*", "**/src/**/*"] } - ] - } - }, - { - "files": ["apps/**/*.ts"], - "rules": { - // Catches static imports - "no-restricted-imports": [ - "error", - { - "patterns": [ - "biwarden_license/**", - "@bitwarden/bit-common/*", - "@bitwarden/bit-web/*", - "**/src/**/*" - ] - } - ], - // Catches dynamic imports, e.g. in routing modules where modules are lazy-loaded - "no-restricted-syntax": [ - "error", - { - "message": "Don't import Bitwarden licensed code into OSS code.", - "selector": "ImportExpression > Literal.source[value=/.*(bitwarden_license|bit-common|bit-web).*/]" - } - ] - } - } - ] -} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d7cca18960a..cc70d3f5b4a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -29,12 +29,12 @@ libs/tools @bitwarden/team-tools-dev bitwarden_license/bit-web/src/app/tools @bitwarden/team-tools-dev bitwarden_license/bit-common/src/tools @bitwarden/team-tools-dev -## Localization/Crowdin (Tools team) -apps/browser/src/_locales @bitwarden/team-tools-dev -apps/browser/store/locales @bitwarden/team-tools-dev -apps/cli/src/locales @bitwarden/team-tools-dev -apps/desktop/src/locales @bitwarden/team-tools-dev -apps/web/src/locales @bitwarden/team-tools-dev +## Localization/Crowdin (Platform and Tools team) +apps/browser/src/_locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev +apps/browser/store/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev +apps/cli/src/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev +apps/desktop/src/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev +apps/web/src/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev ## Vault team files ## apps/browser/src/vault @bitwarden/team-vault-dev @@ -58,6 +58,7 @@ libs/admin-console @bitwarden/team-admin-console-dev ## Billing team files ## apps/browser/src/billing @bitwarden/team-billing-dev +apps/desktop/src/billing @bitwarden/team-billing-dev apps/web/src/app/billing @bitwarden/team-billing-dev libs/angular/src/billing @bitwarden/team-billing-dev libs/common/src/billing @bitwarden/team-billing-dev @@ -96,25 +97,28 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev .github/workflows/scan.yml @bitwarden/team-platform-dev .github/workflows/test.yml @bitwarden/team-platform-dev .github/workflows/version-auto-bump.yml @bitwarden/team-platform-dev +# ESLint custom rules +libs/eslint @bitwarden/team-platform-dev ## Autofill team files ## apps/browser/src/autofill @bitwarden/team-autofill-dev apps/desktop/src/autofill @bitwarden/team-autofill-dev libs/common/src/autofill @bitwarden/team-autofill-dev apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev +apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev # SSH Agent apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-dev @bitwarden/wg-ssh-keys -## Component Library ## -.storybook @bitwarden/team-design-system -libs/components @bitwarden/team-design-system -libs/ui @bitwarden/team-design-system -apps/browser/src/platform/popup/layout @bitwarden/team-design-system -apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-design-system -apps/web/src/app/layouts @bitwarden/team-design-system +## UI Foundation ## +.storybook @bitwarden/team-ui-foundation +libs/components @bitwarden/team-ui-foundation +libs/ui @bitwarden/team-ui-foundation +apps/browser/src/platform/popup/layout @bitwarden/team-ui-foundation +apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-ui-foundation +apps/web/src/app/layouts @bitwarden/team-ui-foundation ## Desktop native module ## apps/desktop/desktop_native @bitwarden/team-platform-dev @@ -127,6 +131,7 @@ apps/web/src/app/key-management @bitwarden/team-key-management-dev apps/browser/src/key-management @bitwarden/team-key-management-dev apps/cli/src/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev +libs/key-management-ui @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev apps/desktop/destkop_native/core/src/biometric/ @bitwarden/team-key-management-dev @@ -162,3 +167,7 @@ apps/web/src/locales/en/messages.json **/*.Dockerfile **/.dockerignore **/entrypoint.sh + +## Overrides +# tsconfig files are potentially dangerous and will be reviewed by platform to prevent misconfigurations +**/tsconfig.json @bitwarden/team-platform-dev diff --git a/.github/codecov.yml b/.github/codecov.yml index b4774407206..b79cdd9f413 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,2 +1,66 @@ ignore: - "**/*.spec.ts" # Tests + +component_management: + default_rules: + statuses: + - type: project + target: auto + individual_components: + - component_id: key-management-biometrics + name: Key Management - Biometrics + paths: + - apps/browser/src/key-management/biometrics/** + - apps/cli/src/key-management/cli-biometrics-service.ts + - apps/desktop/destkop_native/core/src/biometric/** + - apps/desktop/src/key-management/biometrics/** + - apps/desktop/src/services/biometric-message-handler.service.ts + - apps/web/src/app/key-management/web-biometric.service.ts + - libs/key-management/src/biometrics/** + - component_id: key-management-lock + name: Key Management - Lock + paths: + - apps/browser/src/key-management/lock/** + - apps/desktop/src/key-management/lock/** + - apps/web/src/app/key-management/lock/** + - libs/key-management-ui/src/lock/** + - component_id: key-management-ipc + name: Key Management - IPC + paths: + - apps/browser/src/background/nativeMessaging.background.ts + - apps/desktop/src/services/native-messaging.service.ts + - component_id: key-management-key-rotation + name: Key Management - Key Rotation + paths: + - apps/web/src/app/key-management/key-rotation/** + - apps/web/src/app/key-management/migrate-encryption/** + - libs/key-management/src/user-asymmetric-key-regeneration/** + - component_id: key-management-process-reload + name: Key Management - Process Reload + paths: + - apps/web/src/app/key-management/services/web-process-reload.service.ts + - libs/common/src/key-management/services/default-process-reload.service.ts + - component_id: key-management-keys + name: Key Management - Keys + paths: + - libs/key-management/src/kdf-config.service.ts + - libs/key-management/src/key.service.ts + - libs/common/src/key-management/master-password/** + - component_id: key-management-crypto + name: Key Management - Crypto + paths: + - libs/common/src/key-management/crypto/** + - component_id: key-management + name: Key Management + paths: + - apps/browser/src/key-management/** + - apps/browser/src/background/nativeMessaging.background.ts + - apps/cli/src/key-management/** + - apps/desktop/destkop_native/core/src/biometric/** + - apps/desktop/src/key-management/** + - apps/desktop/src/services/biometric-message-handler.service.ts + - apps/desktop/src/services/native-messaging.service.ts + - apps/web/src/app/key-managemen/** + - libs/common/src/key-management/** + - libs/key-management/** + - libs/key-management-ui/** diff --git a/.github/renovate.json b/.github/renovate.json index b5c43cc1d39..f1efcbaffbe 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -48,7 +48,6 @@ "css-loader", "html-loader", "mini-css-extract-plugin", - "ngx-infinite-scroll", "postcss", "postcss-loader", "process", @@ -69,6 +68,26 @@ "commitMessagePrefix": "[deps] Auth:", "reviewers": ["team:team-auth-dev"] }, + { + "matchPackageNames": [ + "@angular-eslint/schematics", + "angular-eslint", + "eslint-config-prettier", + "eslint-import-resolver-typescript", + "eslint-plugin-import", + "eslint-plugin-rxjs-angular", + "eslint-plugin-rxjs", + "eslint-plugin-storybook", + "eslint-plugin-tailwindcss", + "eslint", + "husky", + "lint-staged", + "typescript-eslint" + ], + "description": "Architecture owned dependencies", + "commitMessagePrefix": "[deps] Architecture:", + "reviewers": ["team:dept-architecture"] + }, { "matchPackageNames": [ "@angular-eslint/eslint-plugin-template", @@ -88,9 +107,8 @@ "husky", "lint-staged" ], - "description": "Architecture owned dependencies", - "commitMessagePrefix": "[deps] Architecture:", - "reviewers": ["team:dept-architecture"] + "groupName": "Linting minor-patch", + "matchUpdateTypes": ["minor", "patch"] }, { "matchPackageNames": [ @@ -193,6 +211,8 @@ "@storybook/angular", "@storybook/manager-api", "@storybook/theming", + "@typescript-eslint/utils", + "@typescript-eslint/rule-tester", "@types/react", "autoprefixer", "bootstrap", @@ -207,9 +227,9 @@ "tailwindcss", "zone.js" ], - "description": "Component library owned dependencies", - "commitMessagePrefix": "[deps] Design System:", - "reviewers": ["team:team-design-system"] + "description": "UI Foundation owned dependencies", + "commitMessagePrefix": "[deps] UI Foundation:", + "reviewers": ["team:team-ui-foundation"] }, { "matchPackageNames": [ diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index ff85a30d3f6..d758e6f11c9 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -222,7 +222,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -230,4 +230,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index af24083e973..ae631165db9 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -162,7 +162,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -170,7 +170,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} snap: name: Deploy Snap @@ -283,7 +283,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -291,4 +291,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment_id: ${{ needs.setup.outputs.deployment_id }} + deployment-id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 3e54e79a303..498f8748959 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: ${{ github.event.inputs.release_type }} project-type: ts diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index a1bc36cf85e..6a10bec1ba2 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: ${{ inputs.release_type }} project-type: ts diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index ea0feb10e3d..a5e374395d8 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -47,7 +47,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: 'Initial Release' project-type: ts diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 3285ad468d6..57143747a86 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release_version-check@main + uses: bitwarden/gh-actions/release-version-check@main with: release-type: ${{ inputs.release_type }} project-type: ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72bc3594beb..8c214b99ed3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,13 +85,10 @@ jobs: fail-on-error: true - name: Upload coverage to codecov.io - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 - if: ${{ needs.check-test-secrets.outputs.available == 'true' }} - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 - name: Upload results to codecov.io - uses: codecov/test-results-action@9739113ad922ea0a9abb4b2c0f8bf6a4aa8ef820 # v1.0.1 + uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2 if: ${{ needs.check-test-secrets.outputs.available == 'true' }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -106,15 +103,15 @@ jobs: matrix: os: - ubuntu-22.04 - - macos-latest - - windows-latest + - macos-14 + - windows-2022 steps: - name: Check Rust version run: rustup --version - name: Install gnome-keyring - if: ${{ matrix.os=='ubuntu-latest' }} + if: ${{ matrix.os=='ubuntu-22.04' }} run: | sudo apt-get update sudo apt-get install -y gnome-keyring dbus-x11 @@ -127,7 +124,7 @@ jobs: run: cargo build - name: Test Ubuntu - if: ${{ matrix.os=='ubuntu-latest' }} + if: ${{ matrix.os=='ubuntu-22.04' }} working-directory: ./apps/desktop/desktop_native run: | eval "$(dbus-launch --sh-syntax)" @@ -138,11 +135,41 @@ jobs: cargo test -- --test-threads=1 - name: Test macOS - if: ${{ matrix.os=='macos-latest' }} + if: ${{ matrix.os=='macos-14' }} working-directory: ./apps/desktop/desktop_native run: cargo test -- --test-threads=1 - name: Test Windows - if: ${{ matrix.os=='windows-latest'}} + if: ${{ matrix.os=='windows-2022'}} working-directory: ./apps/desktop/desktop_native/core run: cargo test -- --test-threads=1 + + rust-coverage: + name: Rust Coverage + runs-on: macos-14 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install rust + uses: dtolnay/rust-toolchain@a54c7afa936fefeb4456b2dd8068152669aa8203 # stable + with: + toolchain: stable + components: llvm-tools + + - name: Cache cargo registry + uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5 + with: + workspaces: "apps/desktop/desktop_native -> target" + + - name: Install cargo-llvm-cov + run: cargo install cargo-llvm-cov --version 0.6.16 + + - name: Generate coverage + working-directory: ./apps/desktop/desktop_native + run: cargo llvm-cov --all-features --lcov --output-path lcov.info --workspace --no-cfg-coverage + + - name: Upload to codecov.io + uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1 + with: + files: ./apps/desktop/desktop_native/lcov.info diff --git a/.storybook/main.ts b/.storybook/main.ts index b48a86ba2b2..d98ca06ead3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,7 +1,8 @@ import { dirname, join } from "path"; + import { StorybookConfig } from "@storybook/angular"; -import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; import remarkGfm from "remark-gfm"; +import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; const config: StorybookConfig = { stories: [ @@ -29,6 +30,8 @@ const config: StorybookConfig = { getAbsolutePath("@storybook/addon-designs"), getAbsolutePath("@storybook/addon-interactions"), { + // @storybook/addon-docs is part of @storybook/addon-essentials + // eslint-disable-next-line storybook/no-uninstalled-addons name: "@storybook/addon-docs", options: { mdxPluginOptions: { diff --git a/.storybook/manager.js b/.storybook/manager.js index 409f93ec505..e0ec04fd375 100644 --- a/.storybook/manager.js +++ b/.storybook/manager.js @@ -50,10 +50,14 @@ const darkTheme = create({ }); export const getPreferredColorScheme = () => { - if (!globalThis || !globalThis.matchMedia) return "light"; + if (!globalThis || !globalThis.matchMedia) { + return "light"; + } const isDarkThemePreferred = globalThis.matchMedia("(prefers-color-scheme: dark)").matches; - if (isDarkThemePreferred) return "dark"; + if (isDarkThemePreferred) { + return "dark"; + } return "light"; }; diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b31121e17b..295c290a37a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,6 @@ "**/_locales/*[^n]/messages.json": true }, "rust-analyzer.linkedProjects": ["apps/desktop/desktop_native/Cargo.toml"], - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "eslint.useFlatConfig": true } diff --git a/apps/browser/.eslintrc.json b/apps/browser/.eslintrc.json deleted file mode 100644 index ba960511839..00000000000 --- a/apps/browser/.eslintrc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "env": { - "browser": true, - "webextensions": true - }, - "overrides": [ - { - "files": ["src/**/*.ts"], - "excludedFiles": [ - "src/**/{content,popup,spec}/**/*.ts", - "src/**/autofill/{notification,overlay}/**/*.ts", - "src/**/autofill/**/{autofill-overlay-content,collect-autofill-content,dom-element-visibility,insert-autofill-content}.service.ts", - "src/**/*.spec.ts" - ], - "rules": { - "no-restricted-globals": [ - "error", - { - "name": "window", - "message": "The `window` object is not available in service workers and may not be available within the background script. Consider using `self`, `globalThis`, or another global property instead." - } - ] - } - } - ] -} diff --git a/apps/browser/package.json b/apps/browser/package.json index c37e7c24199..8fc1d733921 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,30 +1,29 @@ { "name": "@bitwarden/browser", - "version": "2025.1.1", + "version": "2025.1.4", "scripts": { "build": "npm run build:chrome", - "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 webpack", - "build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 webpack", - "build:firefox": "cross-env BROWSER=firefox webpack", - "build:opera": "cross-env BROWSER=opera webpack", - "build:safari": "cross-env BROWSER=safari webpack", + "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:firefox": "cross-env BROWSER=firefox NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:opera": "cross-env BROWSER=opera NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", + "build:safari": "cross-env BROWSER=safari NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:watch": "npm run build:watch:chrome", "build:watch:chrome": "npm run build:chrome -- --watch", "build:watch:edge": "npm run build:edge -- --watch", "build:watch:firefox": "npm run build:firefox -- --watch", "build:watch:opera": "npm run build:opera -- --watch", "build:watch:safari": "npm run build:safari -- --watch", - "build:prod:chrome": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:chrome", - "build:prod:edge": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:edge", - "build:prod:firefox": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:firefox", - "build:prod:opera": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:opera", - "build:prod:safari": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:safari", + "build:prod:chrome": "cross-env NODE_ENV=production npm run build:chrome", + "build:prod:edge": "cross-env NODE_ENV=production npm run build:edge", + "build:prod:firefox": "cross-env NODE_ENV=production npm run build:firefox", + "build:prod:opera": "cross-env NODE_ENV=production npm run build:opera", + "build:prod:safari": "cross-env NODE_ENV=production npm run build:safari", "dist:chrome": "npm run build:prod:chrome && mkdir -p dist && ./scripts/compress.ps1 dist-chrome.zip", "dist:edge": "npm run build:prod:edge && mkdir -p dist && ./scripts/compress.ps1 dist-edge.zip", "dist:firefox": "npm run build:prod:firefox && mkdir -p dist && ./scripts/compress.ps1 dist-firefox.zip", "dist:opera": "npm run build:prod:opera && mkdir -p dist && ./scripts/compress.ps1 dist-opera.zip", "dist:safari": "npm run build:prod:safari && ./scripts/package-safari.ps1", - "dist:edge:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:edge", "dist:firefox:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:firefox", "dist:opera:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:opera", "dist:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:safari", diff --git a/apps/browser/scripts/package-safari.ps1 b/apps/browser/scripts/package-safari.ps1 index 075ed606070..ce208478098 100755 --- a/apps/browser/scripts/package-safari.ps1 +++ b/apps/browser/scripts/package-safari.ps1 @@ -52,7 +52,7 @@ foreach ($subBuildPath in $subBuildPaths) { "--verbose", "--force", "--sign", - "E7C9978F6FBCE0553429185C405E61F5380BE8EB", + "4B9662CAB74E8E4F4ECBDD9EDEF2543659D95E3C", "--entitlements", $entitlementsPath ) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 625454de8c6..269c9d9937a 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "توليد عبارة المرور" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "إعادة توليد كلمة المرور" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "قم بتأكيد هويتك" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "خزانتك مقفلة. قم بتأكيد هويتك للمتابعة." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "اطلب إضافة عنصر إذا لم يتم العثور على عنصر في المخزن الخاص بك. ينطبق على جميع حسابات تسجيل الدخول." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "أظهر البطاقات في صفحة التبويبات" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "قائمة عناصر البطاقة في صفحة التبويب لسهولة التعبئة التلقائية." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "إظهار الهويات على صفحة التبويب" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "شراء العضوية المميزة" }, - "premiumPurchaseAlert": { - "message": "يمكنك شراء العضوية المتميزة على bitwarden.com على خزانة الويب. هل تريد زيارة الموقع الآن؟" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "مولد اسم المستخدم" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "استخدم كلمة المرور هذه" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 8e7d2201f28..e06f7117e49 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Keçid ifadələri yarat" }, + "passwordGenerated": { + "message": "Parol yaradıldı." + }, + "passphraseGenerated": { + "message": "Keçid ifadəsi yaradıldı" + }, + "usernameGenerated": { + "message": "İstifadəçi adı yaradıldı" + }, + "emailGenerated": { + "message": "E-poçt yaradıldı" + }, "regeneratePassword": { "message": "Parolu yenidən yarat" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Kimliyi doğrula" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanımırıq. Kimliyinizi doğrulamaq üçün e-poçtunuza göndərilən kodu daxil edin." + }, + "continueLoggingIn": { + "message": "Giriş etməyə davam" + }, "yourVaultIsLocked": { "message": "Seyfiniz kilidlənib. Davam etmək üçün kimliyinizi doğrulayın." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Seyfinizdə tapılmayan elementin əlavə edilməsi soruşulsun. Giriş etmiş bütün hesablara aiddir." }, - "showCardsInVaultView": { - "message": "Kartları, Seyf görünüşündə Avto-doldurma təklifləri olaraq göstər" + "showCardsInVaultViewV2": { + "message": "Kartları, Seyf görünüşündə Avto-doldurma təklifləri olaraq həmişə göstər" }, "showCardsCurrentTab": { "message": "Kartları Vərəq səhifəsində göstər" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Asan avto-doldurma üçün Vərəq səhifəsində kart elementlərini sadalayın." }, - "showIdentitiesInVaultView": { - "message": "Kimlikləri, Seyf görünüşündə Avto-doldurma təklifləri olaraq göstər" + "showIdentitiesInVaultViewV2": { + "message": "Kimlikləri, Seyf görünüşündə Avto-doldurma təklifləri olaraq həmişə göstər" }, "showIdentitiesCurrentTab": { "message": "Vərəq səhifəsində kimlikləri göstər" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Premium satın al" }, - "premiumPurchaseAlert": { - "message": "Premium üzvlüyü bitwarden.com veb seyfində satın ala bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" - }, "premiumPurchaseAlertV2": { "message": "Bitwarden veb tətbiqindəki hesab ayarlarınızda Premium satın ala bilərsiniz." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "İstifadəçi adı yaradıcı" }, + "useThisEmail": { + "message": "Bu e-poçtu istifadə et" + }, "useThisPassword": { "message": "Bu parolu istifadə et" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra enli" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lütfən masaüstü tətbiqinizi güncəlləyin" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Biometrik kilid açmanı istifadə etmək üçün lütfən masaüstü tətbiqinizi güncəlləyin, ya da masaüstü ayarlarında barmaq izi ilə kilid açmanı sıradan çıxardın." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 57496e83f41..04d3ad78982 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Згенерыраваць парольную фразу" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Паўторна генерыраваць пароль" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Праверыць асобу" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Ваша сховішча заблакіравана. Каб працягнуць, пацвердзіце сваю асобу." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Паказваць карткі на старонцы з укладкамі" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Спіс элементаў картак на старонцы з укладкамі для лёгкага аўтазапаўнення." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Паказваць пасведчанні на старонцы з укладкамі" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Купіць прэміум" }, - "premiumPurchaseAlert": { - "message": "Вы можаце купіць прэміяльны статус на bitwarden.com. Перайсці на вэб-сайт зараз?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Генератар імені карыстальніка" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Выкарыстоўваць гэты пароль" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 6e9f992f5ba..1423e64e8a3 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Генериране на парола-фраза" }, + "passwordGenerated": { + "message": "Паролата е генерирана" + }, + "passphraseGenerated": { + "message": "Паролата-фраза е генерирана" + }, + "usernameGenerated": { + "message": "Потребителското име е генерирано" + }, + "emailGenerated": { + "message": "Е-пощата е генерирана" + }, "regeneratePassword": { "message": "Регенериране на паролата" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Потвърждаване на самоличността" }, + "weDontRecognizeThisDevice": { + "message": "Това устройство е непознато. Въведете кода изпратен на е-пощата Ви, за да потвърдите самоличността си." + }, + "continueLoggingIn": { + "message": "Продължаване с вписването" + }, "yourVaultIsLocked": { "message": "Трезорът е заключен — въведете главната си парола, за да продължите." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Питане за добавяне на елемент, ако такъв не бъде намерен в трезора. Прилага се за всички регистрации, в които сте вписан(а)." }, - "showCardsInVaultView": { - "message": "Показване на картите като предложения за авт. попълване в изгледа на трезора" + "showCardsInVaultViewV2": { + "message": "Картите да се показват винаги като предложения за авт. попълване в изгледа на трезора" }, "showCardsCurrentTab": { "message": "Показване на карти в страницата с разделите" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Показване на картите в страницата с разделите, за лесно автоматично попълване." }, - "showIdentitiesInVaultView": { - "message": "Показване на самоличности като предложения за автоматично попълване в изгледа на трезора" + "showIdentitiesInVaultViewV2": { + "message": "Самоличностите да се показват винаги като предложения за автоматично попълване в изгледа на трезора" }, "showIdentitiesCurrentTab": { "message": "Показване на самоличности в страницата с разделите" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Покупка на платен абонамент" }, - "premiumPurchaseAlert": { - "message": "Може да платите абонамента си през сайта bitwarden.com. Искате ли да го посетите сега?" - }, "premiumPurchaseAlertV2": { "message": "Можете да закупите платената версия от настройките на регистрацията си, в приложението по уеб на Битуорден." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Генератор на потребителски имена" }, + "useThisEmail": { + "message": "Използване на тази е-поща" + }, "useThisPassword": { "message": "Използване на тази парола" }, @@ -2285,7 +2303,7 @@ "message": "Потребителят е заключен или отписан" }, "biometricsNotUnlockedDesc": { - "message": "Отключете потребителя в настолното приложение и опитайте отново." + "message": "Отключете потребителя в самостоятелното приложение и опитайте отново." }, "biometricsNotAvailableTitle": { "message": "Отключването чрез биометрични данни не е налично" @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Много широко" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Моля, обновете самостоятелното приложение" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "За да използвате отключването чрез биометрични данни, обновете самостоятелното приложение или изключете отключването чрез пръстов отпечатък в настройките му." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 219071238e9..a4459b177e4 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "পাসওয়ার্ড পুনঃতৈরি করুন" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "আপনার ভল্ট লক করা আছে। চালিয়ে যেতে আপনার মূল পাসওয়ার্ডটি যাচাই করান।" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "প্রিমিয়াম কিনুন" }, - "premiumPurchaseAlert": { - "message": "আপনি bitwarden.com ওয়েব ভল্টে প্রিমিয়াম সদস্যতা কিনতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 81c162e91be..f826d33ae35 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 89ab22c1794..ca168332c12 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -153,13 +153,13 @@ "message": "Copia el número de llicència" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Copia la clau privada" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Copieu la clau pública" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Copia l'empremta digital" }, "copyCustomField": { "message": "Copia $FIELD$", @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Genera frase de pas" }, + "passwordGenerated": { + "message": "Contrasenya generada" + }, + "passphraseGenerated": { + "message": "Frase de pas generada" + }, + "usernameGenerated": { + "message": "Nom d'usuari generat" + }, + "emailGenerated": { + "message": "Correu electrònic generat" + }, "regeneratePassword": { "message": "Regenera contrasenya" }, @@ -530,7 +542,7 @@ "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "Els requisits de la política empresarial s'han aplicat a les opcions del generador.", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -600,7 +612,7 @@ "message": "Obri la web" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "Inicia el lloc web $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifica identitat" }, + "weDontRecognizeThisDevice": { + "message": "No reconeixem aquest dispositiu. Introduïu el codi que us hem enviat al correu electrònic per verificar la identitat." + }, + "continueLoggingIn": { + "message": "Continua l'inici de sessió" + }, "yourVaultIsLocked": { "message": "La caixa forta està bloquejada. Comproveu la contrasenya mestra per continuar." }, @@ -779,10 +797,10 @@ "message": "El vostre compte s'ha creat correctament. Ara ja podeu iniciar sessió." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "S'ha creat el vostre compte nou!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Heu iniciat sessió!" }, "youSuccessfullyLoggedIn": { "message": "Heu iniciat sessió correctament" @@ -831,10 +849,10 @@ "message": "Bitwarden pot emmagatzemar i omplir codis de verificació en dos passos. Copieu i enganxeu la clau en aquest camp." }, "totpHelperWithCapture": { - "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + "message": "Bitwarden pot emmagatzemar i omplir codis de verificació en dos passos. Seleccioneu la icona de la càmera per fer una captura de pantalla del codi QR de l'autenticador d'aquest lloc web o copieu i enganxeu la clau en aquest camp." }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "Més informació sobre els autenticadors" }, "copyTOTP": { "message": "Copia la clau de l'autenticador (TOTP)" @@ -843,7 +861,7 @@ "message": "Sessió tancada" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Heu tancat la sessió del compte." }, "loginExpired": { "message": "La vostra sessió ha caducat." @@ -852,19 +870,19 @@ "message": "Inicia sessió" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Inicia sessió a Bitwarden" }, "restartRegistration": { - "message": "Restart registration" + "message": "Reinicia el registre" }, "expiredLink": { - "message": "Expired link" + "message": "Enllaç caducat" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "És possible que ja tingueu un compte" }, "logOutConfirmation": { "message": "Segur que voleu tancar la sessió?" @@ -888,10 +906,10 @@ "message": "L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a verificar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dues passes a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Fes que el vostre compte siga més segur configurant l'inici de sessió en dos passos a l'aplicació web de Bitwarden." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "Continua cap a l'aplicació web?" }, "editedFolder": { "message": "Carpeta guardada" @@ -978,7 +996,7 @@ "message": "Demana d'afegir els inicis de sessió" }, "vaultSaveOptionsTitle": { - "message": "Save to vault options" + "message": "Opcions de guardar a la caixa forta" }, "addLoginNotificationDesc": { "message": "La \"Notificació per afegir inicis de sessió\" demana automàticament que guardeu els nous inicis de sessió a la vostra caixa forta quan inicieu la sessió per primera vegada." @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Demana afegir un element si no se'n troba cap a la caixa forta. S'aplica a tots els comptes connectats." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Mostra sempre les targetes com a suggeriments d'emplenament automàtic a la vista de la caixa forta" }, "showCardsCurrentTab": { "message": "Mostra les targetes a la pàgina de pestanya" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Llista els elements de la targeta a la pàgina de pestanya per facilitar l'autoemplenat." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Mostra sempre les identitats com a suggeriments d'emplenament automàtic a la vista de la caixa forta" }, "showIdentitiesCurrentTab": { "message": "Mostra les identitats a la pàgina de pestanya" @@ -1005,7 +1023,7 @@ "message": "Llista els elements d'identitat de la pestanya de la pàgina per facilitar l'autoemplenat." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "Feu clic als elements per emplenar automàticament a la vista de la caixa forta" }, "clearClipboard": { "message": "Buida el porta-retalls", @@ -1098,7 +1116,7 @@ "message": "Format de fitxer" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Aquesta exportació de fitxers estarà protegida amb contrasenya i requerirà la contrasenya del fitxer per desxifrar-la." }, "filePassword": { "message": "Contrasenya del fitxer" @@ -1122,11 +1140,11 @@ "message": "\"Contrasenya del fitxer\" i \"Confirma contrasenya del fitxer\" no coincideixen." }, "warning": { - "message": "ADVERTIMENT", + "message": "AVÍS", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Advertència", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1148,7 +1166,7 @@ "message": "Compartit" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." + "message": "Bitwarden per empreses us permet compartir els elements de la vostra caixa forta amb altres usuaris mitjançant una organització. Més informació al lloc web bitwarden.com." }, "moveToOrganization": { "message": "Desplaça a l'organització" @@ -1206,7 +1224,7 @@ "message": "Fitxer" }, "fileToShare": { - "message": "File to share" + "message": "Fitxer per compartir" }, "selectFile": { "message": "Seleccioneu un fitxer" @@ -1242,7 +1260,7 @@ "message": "1 GB d'emmagatzematge xifrat per als fitxers adjunts." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "Accés d’emergència." }, "premiumSignUpTwoStepOptions": { "message": "Opcions propietàries de doble factor com ara YubiKey i Duo." @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Compra Premium" }, - "premiumPurchaseAlert": { - "message": "Podeu comprar la vostra subscripció a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -1287,7 +1302,7 @@ } }, "premiumPriceV2": { - "message": "All for just $PRICE$ per year!", + "message": "Tot per només per $PRICE$ a l'any!", "placeholders": { "price": { "content": "$1", @@ -1317,10 +1332,10 @@ "message": "Introduïu el codi de verificació de 6 dígits de l'aplicació autenticadora." }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Temps d'espera d'autenticació" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "La sessió d'autenticació s'ha esgotat. Reinicieu el procés d'inici de sessió." }, "enterVerificationCodeEmail": { "message": "Introduïu el codi de verificació de 6 dígits que s'ha enviat per correu electrònic a $EMAIL$.", @@ -1390,7 +1405,7 @@ "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Clau de seguretat OTP de Yubico" }, "yubiKeyDesc": { "message": "Utilitzeu una YubiKey per accedir al vostre compte. Funciona amb els dispositius YubiKey 4, 4 Nano, 4C i NEO." @@ -1413,7 +1428,7 @@ "message": "Correu electrònic" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Introduïu el codi que us hem enviat al correu electrònic." }, "selfHostedEnvironment": { "message": "Entorn d'allotjament propi" @@ -1425,7 +1440,7 @@ "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Per a la configuració avançada, podeu especificar l'URL base de cada servei de manera independent." }, "selfHostedEnvFormInvalid": { "message": "You must add either the base Server URL or at least one custom environment." @@ -1440,7 +1455,7 @@ "message": "URL del servidor" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "URL del servidor autoallotjat", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1466,10 +1481,10 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Suggeriments d'emplenament automàtic" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "Mostra suggeriments d'emplenament automàtic als camps del formulari" }, "showInlineMenuIdentitiesLabel": { "message": "Display identities as suggestions" @@ -1478,10 +1493,10 @@ "message": "Display cards as suggestions" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "Mostra suggeriments quan la icona està seleccionada" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "S'aplica a tots els comptes connectats." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Desactiveu la configuració integrada del gestor de contrasenyes del vostre navegador per evitar conflictes." @@ -1502,7 +1517,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "Emplenament automàtic a la càrrega de la pàgina" }, "enableAutoFillOnPageLoad": { "message": "Habilita l'emplenament automàtic en carregar la pàgina" @@ -1514,7 +1529,7 @@ "message": "Els llocs web compromesos o no fiables poden aprofitar-se de l'emplenament automàtic en carregar de la pàgina." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Més informació sobre riscs" }, "learnMoreAboutAutofill": { "message": "Obteniu més informació sobre l'emplenament automàtic" @@ -1544,13 +1559,13 @@ "message": "Obri la caixa forta a la barra lateral" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Emplenament automàtic amb l'últim inici de sessió utilitzat per al lloc web actual" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Emplenament automàtic amb l'última targeta utilitzada per al lloc web actual" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Emplenament automàtic amb l'última identitat utilitzada per al lloc web actual" }, "commandGeneratePasswordDesc": { "message": "Genera i copia una nova contrasenya aleatòria al porta-retalls." @@ -1768,10 +1783,10 @@ "message": "Identitat" }, "typeSshKey": { - "message": "SSH key" + "message": "Clau SSH" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Nou $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1780,7 +1795,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Edita $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1789,7 +1804,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "Mostra $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1801,13 +1816,13 @@ "message": "Historial de les contrasenyes" }, "generatorHistory": { - "message": "Generator history" + "message": "Historial del generador" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Neteja l'historial del generador" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Si continueu, totes les entrades se suprimiran permanentment de l'historial del generador. Esteu segur que voleu continuar?" }, "back": { "message": "Arrere" @@ -1816,7 +1831,7 @@ "message": "Col·leccions" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ col·leccions", "placeholders": { "count": { "content": "$1", @@ -1846,7 +1861,7 @@ "message": "Notes segures" }, "sshKeys": { - "message": "SSH Keys" + "message": "Claus SSH" }, "clear": { "message": "Esborra", @@ -1872,7 +1887,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Domini base (recomanat)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1926,10 +1941,10 @@ "message": "No hi ha cap contrasenya a llistar." }, "clearHistory": { - "message": "Clear history" + "message": "Neteja l'historial" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Res a mostrar" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -1993,10 +2008,10 @@ "message": "Desbloqueja amb codi PIN" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "Estableix el PIN" }, "setYourPinButton": { - "message": "Set PIN" + "message": "Estableix el PIN" }, "setYourPinCode": { "message": "Configureu el vostre codi PIN per desbloquejar Bitwarden. La configuració del PIN es restablirà si tanqueu la sessió definitivament." @@ -2017,7 +2032,7 @@ "message": "Desbloqueja amb biomètrica" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Desbloqueja amb contrasenya mestra" }, "awaitDesktop": { "message": "S’espera confirmació des de l’escriptori" @@ -2029,7 +2044,7 @@ "message": "Bloqueja amb la contrasenya mestra en reiniciar el navegador" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "Sol·licita la contrasenya mestra en reiniciar el navegador" }, "selectOneCollection": { "message": "Heu d'escollir com a mínim una col·lecció." @@ -2041,16 +2056,19 @@ "message": "Clona" }, "passwordGenerator": { - "message": "Password generator" + "message": "Generador de contrasenyes" }, "usernameGenerator": { - "message": "Username generator" + "message": "Generador de nom d'usuari" + }, + "useThisEmail": { + "message": "Use this email" }, "useThisPassword": { - "message": "Use this password" + "message": "Utilitzeu aquesta contrasenya" }, "useThisUsername": { - "message": "Use this username" + "message": "Utilitzeu aquest nom d'usuari" }, "securePasswordGenerated": { "message": "Secure password generated! Don't forget to also update your password on the website." @@ -2096,7 +2114,7 @@ "message": "Element restaurat" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Ja teniu un compte?" }, "vaultTimeoutLogOutConfirmation": { "message": "En tancar la sessió s'eliminarà tot l'accés a la vostra caixa forta i es requerirà una autenticació en línia després del període de temps d'espera. Esteu segur que voleu utilitzar aquesta configuració?" @@ -2108,7 +2126,7 @@ "message": "Ompli automàticament i guarda" }, "fillAndSave": { - "message": "Fill and save" + "message": "Ompli i guarda" }, "autoFillSuccessAndSavedUri": { "message": "Element emplenat automàticament i URI guardat" @@ -2195,10 +2213,10 @@ "message": "Anul·la subscripció" }, "atAnyTime": { - "message": "at any time." + "message": "en qualsevol moment." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "En continuar, acceptes el" }, "and": { "message": "i" @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Dominis bloquejats" }, "excludedDomains": { "message": "Dominis exclosos" @@ -2343,7 +2361,7 @@ "message": "Autofill is blocked for this website." }, "autofillBlockedNoticeGuidance": { - "message": "Change this in settings" + "message": "Canvieu-ho a la configuració" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -2455,7 +2473,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "Are you sure you want to permanently delete this Send?", + "message": "Esteu segur que voleu suprimir permanentment aquest Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { @@ -2511,7 +2529,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send created successfully!", + "message": "Send creat correctament!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { @@ -2604,7 +2622,7 @@ "message": "Es requereix verificació del correu electrònic" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "Correu electrònic verificat" }, "emailVerificationRequiredDesc": { "message": "Heu de verificar el correu electrònic per utilitzar aquesta característica. Podeu verificar el vostre correu electrònic a la caixa forta web." @@ -2646,7 +2664,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "de $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2790,10 +2808,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "S'està exportant la caixa forta de l’organització" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Només s'exportarà la caixa forta de l'organització associada a $ORGANIZATION$. No s'inclouran els elements de les caixes fortes individuals ni d'altres organitzacions.", "placeholders": { "organization": { "content": "$1", @@ -2805,27 +2823,27 @@ "message": "Error" }, "decryptionError": { - "message": "Decryption error" + "message": "Error de desxifrat" }, "couldNotDecryptVaultItemsBelow": { "message": "Bitwarden could not decrypt the vault item(s) listed below." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Contacteu amb el servei d'atenció al client", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "per evitar la pèrdua de dades addicionals.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Genera un nom d'usuari" }, "generateEmail": { - "message": "Generate email" + "message": "Genera correu electrònic" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "El valor ha d'estar entre $MIN$ i $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2839,7 +2857,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Utilitzeu $RECOMMENDED$ caràcters o més per generar una contrasenya segura.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2849,7 +2867,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Utilitzeu paraules $RECOMMENDED$ o més per generar una frase de pas segura.", + "message": " Utilitzeu $RECOMMENDED$ paraules o més per generar una frase de pas segura.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2890,7 +2908,7 @@ "message": "Genera un àlies de correu electrònic amb un servei de reenviament extern." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Domini del correu electrònic", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { @@ -3097,25 +3115,25 @@ "message": "Torna a enviar la notificació" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Veure totes les opcions d'inici de sessió" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Veure totes les opcions d'inici de sessió" }, "notificationSentDevice": { "message": "S'ha enviat una notificació al vostre dispositiu." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "S'ha enviat una notificació al vostre dispositiu" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Assegureu-vos que la vostra caixa forta estiga desbloquejada i que la frase d'empremta digital coincidisca en l'altre dispositiu" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Se us notificarà un vegada s'haja aprovat la sol·licitud" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Necessiteu una altra opció?" }, "loginInitiated": { "message": "S'ha iniciat la sessió" @@ -3175,22 +3193,22 @@ "message": "Configuració d'emplenament automàtic" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "Drecera d'emplenament automàtic" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "Canvia la drecera" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Gestiona les dreceres" }, "autofillShortcut": { "message": "Drecera de teclat d'emplenament automàtic" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "La drecera d'inici de sessió no està configurada. Canvieu-ho a la configuració del navegador." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "La drecera d'emplenament automàtic és $COMMAND$. Gestioneu totes les dreceres a la configuració del navegador.", "placeholders": { "command": { "content": "$1", @@ -3241,25 +3259,25 @@ "message": "Es requereix un identificador SSO de l'organització." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Creant compte en" }, "checkYourEmail": { - "message": "Check your email" + "message": "Comprova el correu" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Seguiu l'enllaç del correu electrònic enviat a" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "i continua creant el compte." }, "noEmail": { - "message": "No email?" + "message": "Sense correu electrònic?" }, "goBack": { "message": "Torna arrere" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "per editar l'adreça de correu electrònic." }, "eu": { "message": "UE", @@ -3302,11 +3320,11 @@ "message": "Dispositiu de confiança" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "No hi ha Sends actius", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "Utilitzeu Send per compartir informació xifrada de manera segura amb qualsevol persona.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -3383,10 +3401,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 camp necessita la vostra atenció." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ camps necessiten la vostra atenció.", "placeholders": { "count": { "content": "$1", @@ -3441,7 +3459,7 @@ "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Canvia a la navegació lateral" }, "skipToContent": { "message": "Vés al contingut" @@ -3503,7 +3521,7 @@ "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "Inici de sessió nou", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { @@ -3519,7 +3537,7 @@ "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "Identitat nova", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { @@ -3613,7 +3631,7 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "S'ha produït un error en connectar amb el servei Duo. Utilitzeu un mètode d'inici de sessió en dos passos diferent o poseu-vos en contacte amb Duo per obtenir ajuda." }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Inicieu DUO i seguiu els passos per finalitzar la sessió." @@ -3708,10 +3726,10 @@ "message": "Clau de pas" }, "accessing": { - "message": "Accessing" + "message": "Accedint a" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Connectat!" }, "passkeyNotCopied": { "message": "La clau de pas no es copiarà" @@ -3723,7 +3741,7 @@ "message": "Verificació requerida pel lloc iniciador. Aquesta funció encara no s'ha implementat per als comptes sense contrasenya mestra." }, "logInWithPasskeyQuestion": { - "message": "Log in with passkey?" + "message": "Inici de sessió amb clau de pas?" }, "passkeyAlreadyExists": { "message": "Ja hi ha una clau de pas per a aquesta aplicació." @@ -3738,7 +3756,7 @@ "message": "No matching logins for this site" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Cerca o guarda la clau de pas com a nou inici de sessió" }, "confirm": { "message": "Confirma-ho" @@ -3750,7 +3768,7 @@ "message": "Guarda la clau de pas com a nou inici de sessió" }, "chooseCipherForPasskeySave": { - "message": "Choose a login to save this passkey to" + "message": "Trieu un inici de sessió per guardar aquesta clau de pas" }, "chooseCipherForPasskeyAuth": { "message": "Choose a passkey to log in with" @@ -3903,7 +3921,7 @@ "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "Voleu continuar cap al Centre d'ajuda?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { @@ -3951,7 +3969,7 @@ "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "Contrasenya guardada!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { @@ -3959,7 +3977,7 @@ "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "Contrasenya actualitzada!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { @@ -3976,7 +3994,7 @@ "message": "Clau de pas suprimida" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Suggeriments d'emplenament automàtic" }, "itemSuggestions": { "message": "Suggested items" @@ -3985,16 +4003,16 @@ "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "La caixa forta està buida" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "No s'ha trobat cap element que coincidisca amb la cerca" }, "clearFiltersOrTryAnother": { "message": "Clear filters or try another search term" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Copia info - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4014,7 +4032,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Més opcions, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4024,7 +4042,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Més opcions - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4057,28 +4075,28 @@ "message": "No values to copy" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Assigna a col·leccions" }, "copyEmail": { - "message": "Copy email" + "message": "Copia el correu electrònic" }, "copyPhone": { - "message": "Copy phone" + "message": "Copia telèfon" }, "copyAddress": { - "message": "Copy address" + "message": "Copia l'adreça" }, "adminConsole": { "message": "Consola d'administració" }, "accountSecurity": { - "message": "Account security" + "message": "Seguretat del compte" }, "notifications": { - "message": "Notifications" + "message": "Notificacions" }, "appearance": { - "message": "Appearance" + "message": "Aparença" }, "errorAssigningTargetCollection": { "message": "S'ha produït un error en assignar la col·lecció de destinació." @@ -4087,7 +4105,7 @@ "message": "S'ha produït un error en assignar la carpeta de destinació." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Mostra elements en $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4097,7 +4115,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Torna a $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4110,7 +4128,7 @@ "message": "Nou" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Suprimeix $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4154,7 +4172,7 @@ "message": "Informació addicional" }, "itemHistory": { - "message": "Item history" + "message": "Historial d'elements" }, "lastEdited": { "message": "Última edició" @@ -4169,13 +4187,13 @@ "message": "Copy Successful" }, "upload": { - "message": "Upload" + "message": "Puja" }, "addAttachment": { - "message": "Add attachment" + "message": "Afegeix adjunt" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "La mida màxima del fitxer és de 500 MB" }, "deleteAttachmentName": { "message": "Delete attachment $NAME$", @@ -4208,7 +4226,7 @@ "message": "Filtres" }, "filterVault": { - "message": "Filter vault" + "message": "Filtra dades" }, "filterApplied": { "message": "One filter applied" @@ -4304,16 +4322,16 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Habilita l'emplenament automàtic en carregar la pàgina?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Targeta de crèdit caducada" }, "cardExpiredMessage": { "message": "If you've renewed it, update the card's information" }, "cardDetails": { - "message": "Card details" + "message": "Dades de la targeta" }, "cardBrandDetails": { "message": "$BRAND$ details", @@ -4328,7 +4346,7 @@ "message": "Enable animations" }, "showAnimations": { - "message": "Show animations" + "message": "Mostra animacions" }, "addAccount": { "message": "Afig compte" @@ -4340,15 +4358,15 @@ "message": "Dades" }, "passkeys": { - "message": "Passkeys", + "message": "Claus de pas", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "Contrasenyes", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Log in with passkey", + "message": "Inicieu sessió amb la clau de pas", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { @@ -4561,10 +4579,10 @@ "message": "Account actions" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "Mostra el nombre de suggeriments d'emplenament automàtic d'inici de sessió a la icona d'extensió" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Mostra accions de còpia ràpida a la caixa forta" }, "systemDefault": { "message": "System default" @@ -4573,16 +4591,16 @@ "message": "Enterprise policy requirements have been applied to this setting" }, "sshPrivateKey": { - "message": "Private key" + "message": "Clau privada" }, "sshPublicKey": { - "message": "Public key" + "message": "Clau pública" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Empremta digital" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Tipus de clau" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4597,7 +4615,7 @@ "message": "RSA 4096-Bit" }, "retry": { - "message": "Retry" + "message": "Torneu-ho a provar" }, "vaultCustomTimeoutMinimum": { "message": "Minimum custom timeout is 1 minute." @@ -4627,10 +4645,10 @@ "message": "Items that have been in trash more than 30 days will automatically be deleted" }, "restore": { - "message": "Restore" + "message": "Restaura" }, "deleteForever": { - "message": "Delete forever" + "message": "Suprimeix per sempre" }, "noEditPermissions": { "message": "You don't have permission to edit this item" @@ -4663,7 +4681,7 @@ "message": "Biometric unlock is currently unavailable for an unknown reason." }, "authenticating": { - "message": "Authenticating" + "message": "S'està autenticant" }, "fillGeneratedPassword": { "message": "Fill generated password", @@ -4682,7 +4700,7 @@ "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "Accent", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { @@ -4702,7 +4720,7 @@ "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Signe del dòlar", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { @@ -4718,15 +4736,15 @@ "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "Asterisc", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Parèntesi esquerre", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Parèntesi dret", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { @@ -4766,7 +4784,7 @@ "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Barra Invertida", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { @@ -4774,7 +4792,7 @@ "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Punt i coma", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { @@ -4786,23 +4804,23 @@ "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Menor que", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Major que", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Coma", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Punt", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Signe d'interrogació", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { @@ -4810,22 +4828,22 @@ "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Minúscules" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Majúscules" }, "generatedPassword": { - "message": "Generated password" + "message": "Contrasenya generada" }, "compactMode": { - "message": "Compact mode" + "message": "Mode compacte" }, "beta": { "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Noticia important" }, "setupTwoStepLogin": { "message": "Set up two-step login" @@ -4837,7 +4855,7 @@ "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "Recorda-m'ho més tard" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -4864,9 +4882,15 @@ "message": "Extension width" }, "wide": { - "message": "Wide" + "message": "Ample" }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 0a05974279a..9c427edf5cf 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Vygenerovat heslovou frázi" }, + "passwordGenerated": { + "message": "Heslo bylo vygenerováno" + }, + "passphraseGenerated": { + "message": "Heslová fráze byla vygenerována" + }, + "usernameGenerated": { + "message": "Uživatelské jméno bylo vygenerováno" + }, + "emailGenerated": { + "message": "E-mail byl vygenerován" + }, "regeneratePassword": { "message": "Vygenerovat jiné heslo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Ověřit identitu" }, + "weDontRecognizeThisDevice": { + "message": "Toto zařízení nepoznáváme. Zadejte kód zaslaný na Váš e-mail pro ověření Vaší totožnosti." + }, + "continueLoggingIn": { + "message": "Pokračovat v přihlášení" + }, "yourVaultIsLocked": { "message": "Váš trezor je uzamčen. Pro pokračování musíte zadat hlavní heslo." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Požádá o přidání položky, pokud nebyla nalezena v trezoru. Platí pro všechny přihlášené účty." }, - "showCardsInVaultView": { - "message": "Zobrazit karty jako návrhy automatického vyplňování v zobrazení trezoru" + "showCardsInVaultViewV2": { + "message": "Vždy zobrazit karty jako návrhy automatického vyplňování v zobrazení trezoru" }, "showCardsCurrentTab": { "message": "Zobrazit platební karty na obrazovce Karta" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Pro snadné vyplnění zobrazí platební karty na obrazovce Karta." }, - "showIdentitiesInVaultView": { - "message": "Zobrazit identity jako návrhy automatického vyplňování v zobrazení trezoru" + "showIdentitiesInVaultViewV2": { + "message": "Vždy zobrazit identity jako návrhy automatického vyplňování v zobrazení trezoru" }, "showIdentitiesCurrentTab": { "message": "Zobrazit identity na obrazovce Karta" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Zakoupit členství Premium" }, - "premiumPurchaseAlert": { - "message": "Prémiové členství můžete zakoupit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" - }, "premiumPurchaseAlertV2": { "message": "Premium si můžete zakoupit v nastavení účtu ve webové aplikaci Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generátor uživatelského jména" }, + "useThisEmail": { + "message": "Použít tento e-mail" + }, "useThisPassword": { "message": "Použít toto heslo" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra široký" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aktualizujte aplikaci pro stolní počítač" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Chcete-li použít biometrické odemknutí, aktualizujte aplikaci pro stolní počítač nebo v nastavení vypněte odemknutí otiskem prstů." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index cbaa31fea30..31208e2e020 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -29,7 +29,7 @@ "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Croeso nôl" }, "setAStrongPassword": { "message": "Gosod cyfrinair cryf" @@ -171,7 +171,7 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copïo'r wefan" }, "copyNotes": { "message": "Copy notes" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Ailgynhyrchu cyfrinair" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Gwirio'ch hunaniaeth" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Mae eich cell ar glo. Gwiriwch eich hunaniaeth i barhau." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Prynu aelodaeth uwch" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4075,7 +4093,7 @@ "message": "Account security" }, "notifications": { - "message": "Notifications" + "message": "Hysbysiadau" }, "appearance": { "message": "Golwg" @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 628ce983c09..b553afdbe2f 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generér adgangssætning" }, + "passwordGenerated": { + "message": "Adgangskode genereret" + }, + "passphraseGenerated": { + "message": "Adgangssætning genereret" + }, + "usernameGenerated": { + "message": "Brugernavn genereret" + }, + "emailGenerated": { + "message": "E-mail genereret" + }, "regeneratePassword": { "message": "Regenerér adgangskode" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Bekræft identitet" }, + "weDontRecognizeThisDevice": { + "message": "Denne enhed er ikke genkendt. Angiv koden i den tilsendte e-mail for at bekræfte identiteten." + }, + "continueLoggingIn": { + "message": "Fortsæt med at logge ind" + }, "yourVaultIsLocked": { "message": "Din boks er låst. Bekræft din identitet for at fortsætte." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Anmod om at tilføje et emne, hvis intet ikke findes i boksen. Gælder alle indloggede konti." }, - "showCardsInVaultView": { - "message": "Vis kort som Autoudfyldningsforslag ved Boks-visning" + "showCardsInVaultViewV2": { + "message": "Vis altid kort som Autoudfyldningsforslag ved Boks-visning" }, "showCardsCurrentTab": { "message": "Vis kort på fanebladet" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Vis kortemner på siden Fane for nem autoudfyldning." }, - "showIdentitiesInVaultView": { - "message": "Vis identiteter som Autoudfyldningsforslag ved Boks-visning" + "showIdentitiesInVaultViewV2": { + "message": "Vis altid identiteter som Autoudfyldningsforslag ved Boks-visning" }, "showIdentitiesCurrentTab": { "message": "Vis identiteter på fanebladet" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Køb premium" }, - "premiumPurchaseAlert": { - "message": "Du kan købe premium-medlemskab i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?" - }, "premiumPurchaseAlertV2": { "message": "Der kan købes Premium fra kontoindstillingerne via Bitwarden web-appen." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Brugernavngenerator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Anvend denne adgangskode" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra bred" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Opdatér venligst computerapplikationen" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "For brug af biometrisk oplåsning skal computerapplikationen opdateres eller fingeraftryksoplåsning deaktiveres i computerindstillingerne." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 86d9d513e40..1dca0804873 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Passphrase generieren" }, + "passwordGenerated": { + "message": "Passwort generiert" + }, + "passphraseGenerated": { + "message": "Passphrase generiert" + }, + "usernameGenerated": { + "message": "Benutzername generiert" + }, + "emailGenerated": { + "message": "E-Mail-Adresse generiert" + }, "regeneratePassword": { "message": "Passwort neu generieren" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identität verifizieren" }, + "weDontRecognizeThisDevice": { + "message": "Wir erkennen dieses Gerät nicht. Gib den an deine E-Mail-Adresse gesendeten Code ein, um deine Identität zu verifizieren." + }, + "continueLoggingIn": { + "message": "Anmeldung fortsetzen" + }, "yourVaultIsLocked": { "message": "Dein Tresor ist gesperrt. Verifiziere deine Identität, um fortzufahren." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Nach dem Hinzufügen eines Eintrags fragen, wenn er nicht in deinem Tresor gefunden wurde. Gilt für alle angemeldeten Konten." }, - "showCardsInVaultView": { - "message": "Karten als Vorschläge zum Auto-Ausfüllen in der Tresor-Ansicht anzeigen" + "showCardsInVaultViewV2": { + "message": "Karten immer als Auto-Ausfüllen-Vorschläge in der Tresor-Ansicht anzeigen" }, "showCardsCurrentTab": { "message": "Karten auf Tab Seite anzeigen" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Karten-Einträge auf der Tab Seite anzeigen, um das Auto-Ausfüllen zu vereinfachen." }, - "showIdentitiesInVaultView": { - "message": "Identitäten als Vorschläge zum Auto-Ausfüllen in der Tresor-Ansicht anzeigen" + "showIdentitiesInVaultViewV2": { + "message": "Identitäten immer als Auto-Ausfüllen-Vorschläge in der Tresor-Ansicht anzeigen" }, "showIdentitiesCurrentTab": { "message": "Identitäten auf Tab Seite anzeigen" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Premium-Mitgliedschaft kaufen" }, - "premiumPurchaseAlert": { - "message": "Du kannst deine Premium-Mitgliedschaft im Bitwarden.com Web-Tresor kaufen. Möchtest du die Website jetzt besuchen?" - }, "premiumPurchaseAlertV2": { "message": "Du kannst Premium über deine Kontoeinstellungen in der Bitwarden Web-App kaufen." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Benutzernamen-Generator" }, + "useThisEmail": { + "message": "Diese E-Mail-Adresse verwenden" + }, "useThisPassword": { "message": "Dieses Passwort verwenden" }, @@ -2811,7 +2829,7 @@ "message": "Bitwarden konnte folgende(n) Tresor-Eintrag/Einträge nicht entschlüsseln." }, "contactCSToAvoidDataLossPart1": { - "message": "Kontaktiere den Kundensupport", + "message": "Kontaktiere unser Customer Success Team", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { @@ -3927,7 +3945,7 @@ "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Das Ignorieren dieser Option kann zu Konflikten zwischen dem Bitwarden Auto-Ausfüllen Menü und dem Browser führen.", + "message": "Das Ignorieren dieser Option kann zu Konflikten zwischen den Bitwarden Vorschlägen zum Auto-Ausfüllen und denen deines Browsers führen.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3982,7 +4000,7 @@ "message": "Vorgeschlagene Einträge" }, "autofillSuggestionsTip": { - "message": "Speichere einen Login-Eintrag für diese Seite zum automatischen Ausfüllen" + "message": "Speichere einen Zugangsdaten-Eintrag für diese Seite zum automatischen Ausfüllen" }, "yourVaultIsEmpty": { "message": "Dein Tresor hat keine Einträge" @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra breit" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Bitte aktualisiere deine Desktop-Anwendung" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Um biometrisches Entsperren zu verwenden, aktualisiere bitte deine Desktop-Anwendung oder deaktiviere die Entsperrung per Fingerabdruck in den Desktop-Einstellungen." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 16fd5094fdd..7d2afbf9969 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Δημιουργία φράσης πρόσβασης" }, + "passwordGenerated": { + "message": "Ο κωδικός δημιουργήθηκε" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Επαναδημιουργία κωδικού πρόσβασης" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Επιβεβαίωση ταυτότητας" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Το vault σας είναι κλειδωμένο. Επαληθεύστε τον κύριο κωδικό πρόσβασης για να συνεχίσετε." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ζητήστε να προσθέσετε ένα αντικείμενο αν δε βρεθεί στο θησαυ/κιό σας. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, - "showCardsInVaultView": { - "message": "Εμφάνιση καρτών ως προτάσεις αυτόματης συμπλήρωσης στην προβολή Θησαυ/κίου" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Εμφάνιση καρτών στη σελίδα Καρτέλας" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Εμφάνισε τα αντικείμενα κάρτες στη σελίδα Καρτέλα για εύκολη αυτόματη συμπλήρωση." }, - "showIdentitiesInVaultView": { - "message": "Εμφάνιση ταυτοτήτων ως προτάσεις αυτόματης συμπλήρωσης στην προβολή Θησαυ/κίου" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Εμφάνιση ταυτοτήτων στη σελίδα καρτέλας" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Αγορά Premium έκδοσης" }, - "premiumPurchaseAlert": { - "message": "Μπορείτε να αγοράσετε συνδρομή Premium στο διαδικτυακό θησαυ/κιο του bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" - }, "premiumPurchaseAlertV2": { "message": "Μπορείτε να αγοράσετε το Premium από τις ρυθμίσεις του λογαριασμού σας στην διαδικτυακή εφαρμογή Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Γεννήτρια ονόματος χρήστη" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Χρήση αυτού του κωδικού πρόσβασης" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Εξαιρετικά φαρδύ" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6b1764289f8..8698315b57c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -3105,12 +3123,18 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "youWillBeNotifiedOnceTheRequestIsApproved": { "message": "You will be notified once the request is approved" }, @@ -3120,6 +3144,9 @@ "loginInitiated": { "message": "Login initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "exposedMasterPassword": { "message": "Exposed Master Password" }, @@ -4128,15 +4155,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, @@ -4869,6 +4887,15 @@ "extraWide": { "message": "Extra wide" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "updateDesktopAppOrDisableFingerprintDialogTitle": { "message": "Please update your desktop application" }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 4ea532f1e35..6ede9cab724 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 38724849736..1be001abea4 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identifies as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 2f87902cedb..df5f38f878c 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerar contraseña" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar identidad" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Tu caja fuerte está bloqueada. Verifica tu identidad para continuar." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Pide que se agregue un elemento si no se encuentra uno en su caja fuerte. Se aplica a todas las cuentas que hayan iniciado sesión." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Mostrar las tarjetas en la pestaña" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Listar los elementos de tarjetas en la página para facilitar el auto-rellenado." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Mostrar las identidades en la página" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Comprar Premium" }, - "premiumPurchaseAlert": { - "message": "Puedes comprar la membresía Premium en la caja fuerte web de bitwarden.com. ¿Quieres visitar el sitio web ahora?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generador de nombres de usuario" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Usar esta contraseña" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 5ee8566dc33..a5c69ed7cac 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Genereeri parool uuesti" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identiteedi kinnitamine" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Hoidla on lukus. Jätkamiseks sisesta ülemparool." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Kuva \"Kaart\" vaates kaardiandmed" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kuvab \"Kaart\" vaates kaardiandmeid, et neid saaks kiiresti sisestada" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Kuva \"Kaart\" vaates identiteete" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Osta Premium" }, - "premiumPurchaseAlert": { - "message": "Bitwardeni premium versiooni saab osta bitwarden.com veebihoidlas. Avan veebihoidla?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Kasutajanime genereerija" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Kasuta seda parooli" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index d7bcfc2838e..7f0da4e1d41 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Berrezarri pasahitza" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Zure identitatea egiaztatu" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Zure kutxa gotorra blokeatuta dago. Egiaztatu zure identitatea jarraitzeko." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Erakutsi txartelak fitxa orrian" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Erakutsi elementuen txartelak fitxa orrian, erraz auto-betetzeko." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Erakutsi identitateak fitxa orrian" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Premium erosi" }, - "premiumPurchaseAlert": { - "message": "Zure premium bazkidetza bitwarden.com webguneko kutxa gotorrean ordaindu dezakezu. Orain bisitatu nahi duzu webgunea?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 3cb1581b98f..034a79b3d35 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "تولید مجدد کلمه عبور" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "تأیید هویت" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "گاوصندوق شما قفل شده است. برای ادامه هویت خود را تأیید کنید." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "نمایش کارت‌ها در صفحه برگه" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "برای پر کردن خودکار آسان، موارد کارت را در صفحه برگه فهرست کن." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "نشان دادن هویت در صفحه برگه" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "خرید پرمیوم" }, - "premiumPurchaseAlert": { - "message": "شما می‌توانید عضویت پرمیوم را از گاوصندوق وب bitwarden.com خریداری کنید. مایلید اکنون از وب‌سایت بازید کنید؟" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 9e2f18ea72e..8e327cbe222 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -120,7 +120,7 @@ "message": "Kopioi salasana" }, "copyPassphrase": { - "message": "Kopioi salalause" + "message": "Kopioi salauslauseke" }, "copyNote": { "message": "Kopioi merkinnät" @@ -443,7 +443,19 @@ "message": "Luo salasana" }, "generatePassphrase": { - "message": "Luo salalause" + "message": "Luo salauslauseke" + }, + "passwordGenerated": { + "message": "Salasana luotiin" + }, + "passphraseGenerated": { + "message": "Salauslauseke luotiin" + }, + "usernameGenerated": { + "message": "Käyttäjätunnus luotiin" + }, + "emailGenerated": { + "message": "Sähköpostiosoite luotu" }, "regeneratePassword": { "message": "Luo uusi salasana" @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Vahvista henkilöllisyytesi" }, + "weDontRecognizeThisDevice": { + "message": "Emme tunnista tätä laitetta. Anna sähköpostiisi lähetetty koodi henkilöllisyytesi vahvistamiseksi." + }, + "continueLoggingIn": { + "message": "Jatka kirjautumista" + }, "yourVaultIsLocked": { "message": "Holvisi on lukittu. Jatka vahvistamalla henkilöllisyytesi." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ehdota kohteen tallennusta, jos holvistasi ei vielä löydy vastaavaa kohdetta. Koskee kaikkia kirjautuneita tilejä." }, - "showCardsInVaultView": { - "message": "Näytä kortit automaattitäytön ehdotuksina Holvi-näkymässä" + "showCardsInVaultViewV2": { + "message": "Näytä aina kortit automaattisen täytön ehdotuksina Holvi-näkymässä" }, "showCardsCurrentTab": { "message": "Näytä kortit välilehtiosiossa" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Näytä kortit Välilehti-sivulla automaattitäytön helpottamiseksi." }, - "showIdentitiesInVaultView": { - "message": "Näytä henkilöllisyydet automaattitäytön ehdotuksina Holvi-sivulla." + "showIdentitiesInVaultViewV2": { + "message": "Näytä aina identiteetit automaattisen täytön ehdotuksina Holvi-näkymässä" }, "showIdentitiesCurrentTab": { "message": "Näytä henkilöllisyydet välilehtiosiossa" @@ -1005,7 +1023,7 @@ "message": "Näytä henkilöllisyydet Välilehti-sivulla automaattitäytön helpottamiseksi." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "Valitse kohteita täyttääksesi tiedot automaattisesti Holvi-näkymässä" }, "clearClipboard": { "message": "Tyhjennä leikepöytä", @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Osta Premium" }, - "premiumPurchaseAlert": { - "message": "Voit ostaa Premium-jäsenyyden bitwarden.com-verkkoholvista. Haluatko avata sivuston nyt?" - }, "premiumPurchaseAlertV2": { "message": "Voit ostaa Premiumin tiliasetuksistasi Bitwardenin verkkosovelluksen kautta." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Käyttäjätunnusgeneraattori" }, + "useThisEmail": { + "message": "Käytä tätä sähköpostia" + }, "useThisPassword": { "message": "Käytä tätä salasanaa" }, @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Estetyt verkkotunnukset" }, "excludedDomains": { "message": "Ohitettavat verkkotunnukset" @@ -2337,13 +2355,13 @@ "message": "Bitwarden ei pyydä kirjautumistietojen tallennusta näillä verkkotunnuksilla. Koskee kaikkia kirjautuneita tilejä. Ota muutokset käyttöön päivittämällä sivu." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "Näille sivustoille ei tarjota automaattista täyttöä eikä muita siihen liittyviä ominaisuuksia. Sinun on päivitettävä sivu, jotta muutokset tulevat voimaan." }, "autofillBlockedNoticeV2": { - "message": "Autofill is blocked for this website." + "message": "Automaattitäyttö on estetty tällä sivustolla." }, "autofillBlockedNoticeGuidance": { - "message": "Change this in settings" + "message": "Muuta tätä asetuksissa" }, "websiteItemLabel": { "message": "Verkkotunnus $number$ (URI)", @@ -2364,7 +2382,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Estetyn verkkotunnuksen muutokset tallennettu" }, "excludedDomainsSavedSuccess": { "message": "Rajoitettujen verkkotunnusten muutokset tallennettiin" @@ -2805,17 +2823,17 @@ "message": "Virhe" }, "decryptionError": { - "message": "Decryption error" + "message": "Salauksen purkuvirhe" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden ei pystynyt purkamaan alla lueteltuja holvin kohteita." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Ota yhteyttä asiakkaaseen", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "lisätietojen menettämisen välttämiseksi.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { @@ -2849,7 +2867,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Käytä $RECOMMENDED$ tai useampaa sanaa vahvan salalauseen luomiseen.", + "message": " Käytä $RECOMMENDED$ tai useampaa sanaa vahvan salauslausekkeen luomiseen.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3976,10 +3994,10 @@ "message": "Pääsyavain poistettiin" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Automaattitäytön ehdotukset" }, "itemSuggestions": { - "message": "Suggested items" + "message": "Ehdotetut kohteet" }, "autofillSuggestionsTip": { "message": "Tallenna tälle sivustolle automaattisesti täytettävä kirjautumistieto." @@ -4636,22 +4654,22 @@ "message": "Sinulla ei ole oikeutta muokata tätä kohdetta" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Biometrinen avaus ei ole käytettävissä, koska PIN-koodi tai salasanan lukituksen avaus vaaditaan ensin." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Biometrinen avaus ei tällä hetkellä ole käytettävissä." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biometrinen avaus ei ole käytettävissä, koska järjestelmätiedostoja ei ole määritetty." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Biometrinen avaus ei ole käytettävissä, koska järjestelmätiedostoja ei ole määritetty." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Biometrinen avaus ei ole käytettävissä, koska Bitwardenin työpöytäsovellus on suljettu." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Biometrinen avaus ei ole käytettävissä, koska sitä ei ole otettu käyttöön osoitteelle $EMAIL$ Bitwardenin työpöytäsovelluksessa.", "placeholders": { "email": { "content": "$1", @@ -4660,7 +4678,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Biometrinen avaus ei ole tällä hetkellä käytettävissä tuntemattomasta syystä." }, "authenticating": { "message": "Todennetaan" @@ -4834,13 +4852,13 @@ "message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Voit ottaa käyttöön kaksivaiheisen kirjautumisen vaihtoehtoisena tapana suojata tilisi, tai vaihtaa sähköpostisi sellaiseen, johon sinulla on pääsy." }, "remindMeLater": { "message": "Muistuta myöhemmin" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Onko sinulla luotettava pääsy sähköpostiisi, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4849,10 +4867,10 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Ei ole" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Kyllä on" }, "turnOnTwoStepLogin": { "message": "Ota kaksivaiheinen kirjautuminen käyttöön" @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Erittäin leveä" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Päivitä työpöytäsovellus" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Käyttääksesi biometristä avausta, päivitä työpöytäsovelluksesi tai poista tunnistelauseke käytöstä työpöydän asetuksista." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 62e962e309a..a37756ece9a 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Muling I-generate ang Password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "I-verify ang pagkakakilanlan" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Naka-lock ang iyong vault. Patunayan ang iyong pagkakakilanlan upang magpatuloy." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Hilingin na magdagdag ng isang item kung ang isa ay hindi mahanap sa iyong vault. Nalalapat sa lahat ng naka-log in na account." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Ipakita ang mga card sa Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Itala ang mga item ng card sa Tab page para sa madaling auto-fill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Ipakita ang mga pagkatao sa Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Bilhin ang Premium" }, - "premiumPurchaseAlert": { - "message": "Maaari kang mamili ng membership sa Premium sa website ng bitwarden.com. Gusto mo bang bisitahin ang website ngayon?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index b1ebfe05fe1..26d3c1c352e 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Générer une phrase de passe" }, + "passwordGenerated": { + "message": "Mot de passe généré" + }, + "passphraseGenerated": { + "message": "Phrase de passe générée" + }, + "usernameGenerated": { + "message": "Nom d'utilisateur généré" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Régénérer un mot de passe" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Vérifier l'identité" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continuer à se connecter" + }, "yourVaultIsLocked": { "message": "Votre coffre est verrouillé. Vérifiez votre identité pour continuer." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Demande l'ajout d'un élément si celui-ci n'est pas trouvé dans votre coffre. S'applique à tous les comptes connectés." }, - "showCardsInVaultView": { - "message": "Afficher les cartes de paiement en tant que suggestions de saisie automatique dans la vue du coffre" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Afficher les cartes de paiement sur la Page d'onglet" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Liste les éléments des cartes de paiement sur la Page d'onglet pour faciliter la saisie automatique." }, - "showIdentitiesInVaultView": { - "message": "Afficher les identités en tant que suggestions de saisie automatique dans la vue du coffre" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Afficher les identités sur la Page d'onglet" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Acheter Premium" }, - "premiumPurchaseAlert": { - "message": "Vous pouvez acheter une adhésion Premium sur le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" - }, "premiumPurchaseAlertV2": { "message": "Vous pouvez acheter la version Premium depuis les paramètres de votre compte dans l'application web Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Générateur de nom d'utilisateur" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Utiliser ce mot de passe" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Très large" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 3d352359e69..423f5f4c471 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Xerar frase de contrasinal" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Volver xerar contrasinal" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar identidade" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "A túa caixa forte está bloqueada. Verifica a túa identidade para continuar." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ofrecer gardar un elemento se non se atopa na caixa forte. Aplica a tódalas sesións iniciadas." }, - "showCardsInVaultView": { - "message": "Na caixa forte, amosar tarxetas como suxestións de Autoenchido" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Amosar tarxetas na pestana" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Lista na pestana actual as tarxetas gardadas para autoenchido." }, - "showIdentitiesInVaultView": { - "message": "Na caixa forte, amosar identidades como suxestións de Autoenchido" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Amosar identidades na pestana" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Adquirir Prémium" }, - "premiumPurchaseAlert": { - "message": "Podes adquirir o plan Prémium na aplicación web de bitwarden.com. Queres visitala agora mesmo?" - }, "premiumPurchaseAlertV2": { "message": "Podes adquirir o plan Prémium dende os axustes de conta da aplicación web de Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Xerador de nomes de usuario" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Usar este contrasinal" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Moi ancho" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index b66d0a4aa24..93600eaf6a9 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "צור סיסמה חדשה" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "אימות זהות" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "הכספת שלך נעולה. הזן את הסיסמה הראשית שלך כדי להמשיך." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "רכוש פרימיום" }, - "premiumPurchaseAlert": { - "message": "באפשרותך לרכוש מנוי פרימיום בכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 8927c78e16b..071c7acdb0f 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate Password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "पहचान सत्यापित करें" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "आपकी वॉल्ट लॉक हो गई है। जारी रखने के लिए अपने मास्टर पासवर्ड को सत्यापित करें।" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "टैब पेज पर कार्ड दिखाएं" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "आसान ऑटो-फिल के लिए टैब पेज पर कार्ड आइटम सूचीबद्ध करें।" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "टैब पेज पर पहचान दिखाएं" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "आप bitwarden.com वेब वॉल्ट पर प्रीमियम सदस्यता खरीद सकते हैं।क्या आप अब वेबसाइट पर जाना चाहते हैं?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 20583c4cbbd..a8f9c8b672f 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generiraj frazu lozinke" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Ponovno generiraj lozinku" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Potvrdi identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Tvoj trezor je zaključan. Potvrdi glavnu lozinku za nastavak." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Pitaj za dodavanje stavke ako nije pronađena u tvojem trezoru. Primjenjuje se na sve prijavljene račune." }, - "showCardsInVaultView": { - "message": "Prikaži kartice kao prijedloge za auto-ispunu u prikazu trezora" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Prikaži platne kartice" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Prikazuj platne kartice za jednostavnu auto-ispunu." }, - "showIdentitiesInVaultView": { - "message": "Prikaži identitete kao prijedloge za auto-ispunu u prikazu trezora" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Prikaži identitete" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Kupi premium članstvo" }, - "premiumPurchaseAlert": { - "message": "Možeš kupiti premium članstvo na web trezoru. Želiš li sada posjetiti bitwarden.com?" - }, "premiumPurchaseAlertV2": { "message": "Premium možeš kupiti u postavkama računa na Bitwarden web aplikaciji." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generator korisničkih imena" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Koristi ovu lozinku" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra široko" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index bcde4db3c4f..9ac2cea07ce 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Jelmondat generálás" }, + "passwordGenerated": { + "message": "A jelszó generálásra került." + }, + "passphraseGenerated": { + "message": "A jelmondat generálásra került." + }, + "usernameGenerated": { + "message": "A felhasználónév generálásra került." + }, + "emailGenerated": { + "message": "Az email generálásra került." + }, "regeneratePassword": { "message": "Jelszó újragenerálása" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Személyazonosság ellenőrzése" }, + "weDontRecognizeThisDevice": { + "message": "Nem ismerhető fel ez az eszköz. Írjuk be az email címünkre küldött kódot a személyazonosság igazolásához." + }, + "continueLoggingIn": { + "message": "A bejelentkezés folytatása" + }, "yourVaultIsLocked": { "message": "A széf zárolásra került. A folytatáshoz meg kell adni a mesterjelszót." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Egy elem hozzáadásának kérése, ha az nem található a széfben. Minden bejelentkezett fiókra vonatkozik." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Mindig jelenítse meg a kártyákat automatikus kitöltési javaslatként a Széf nézetben" }, "showCardsCurrentTab": { "message": "Kártyák megjelenítése a Fül oldalon" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kártyaelemek listázása a Fül oldalon a könnyű automatikus kitöltéshez." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Mindig jelenítse meg a személyazonosságokat automatikus kitöltési javaslatként a Széf nézetben" }, "showIdentitiesCurrentTab": { "message": "Azonosítások megjelenítése a Fül oldalon" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Prémium funkció megvásárlása" }, - "premiumPurchaseAlert": { - "message": "A prémium tagság megvásárolható a bitwarden.com webes széfben. Szeretnénk felkeresni a webhelyet most?" - }, "premiumPurchaseAlertV2": { "message": "Prémium szolgáltatást vásárolhatunk a Bitwarden webalkalmazás fiókbeállításai között." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Felhasználónév generátor" }, + "useThisEmail": { + "message": "Ezen email használata" + }, "useThisPassword": { "message": "Jelszó használata" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra széles" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Frissítsük az asztali alkalmazást." + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "A biometrikus feloldás használatához frissítsük az asztali alkalmazást vagy tiltsuk le az ujjlenyomatos feloldást az asztali beállításokban." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index fedfb0cec1f..b059303d5a2 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Buat frasa sandi" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Buat Ulang Kata Sandi" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifikasi Identitas Anda" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Brankas Anda terkunci. Verifikasi kata sandi utama Anda untuk melanjutkan." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Tanyakan untuk menambah sebuah benda jika benda itu tidak ditemukan di brankas Anda. Diterapkan ke seluruh akun yang telah masuk." }, - "showCardsInVaultView": { - "message": "Tampilkan kartu sebagai saran isi otomatis pada tampilan Brankas" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Tamplikan kartu pada halaman Tab" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Buat tampilan daftar benda dari kartu pada halaman Tab untuk isi otomatis yang mudah." }, - "showIdentitiesInVaultView": { - "message": "Tampilkan identitas sebagai saran isi otomatis pada tampilan Brankas" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Tampilkan identitas pada halaman Tab" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Beli Keanggotaan Premium" }, - "premiumPurchaseAlert": { - "message": "Anda dapat membeli keanggotaan premium di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" - }, "premiumPurchaseAlertV2": { "message": "Anda dapat membeli Premium dari pilihan akun Anda pada aplikasi web Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Pembuat nama pengguna" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Gunakan kata sandi ini" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra lebar" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 53b94a951c1..0765cd0e419 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Genera passphrase" }, + "passwordGenerated": { + "message": "Parola d'accesso generata" + }, + "passphraseGenerated": { + "message": "Frase d'accesso generata" + }, + "usernameGenerated": { + "message": "Nome utente generato" + }, + "emailGenerated": { + "message": "E-mail generata" + }, "regeneratePassword": { "message": "Rigenera password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifica identità" }, + "weDontRecognizeThisDevice": { + "message": "Non riconosciamo questo dispositivo. Inserisci il codice inviato alla tua e-mail per verificare la tua identità." + }, + "continueLoggingIn": { + "message": "Continua l'accesso" + }, "yourVaultIsLocked": { "message": "La tua cassaforte è bloccata. Verifica la tua identità per continuare." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Chiedi di creare un nuovo elemento se non ce n'è uno nella tua cassaforte. Si applica a tutti gli account sul dispositivo." }, - "showCardsInVaultView": { - "message": "Mostra le carte come suggerimenti di riempimento automatico nella vista cassaforte" + "showCardsInVaultViewV2": { + "message": "Mostra sempre le carte come suggerimenti di riempimento automatico nella vista cassaforte" }, "showCardsCurrentTab": { "message": "Mostra le carte nella sezione Scheda" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Mostra le carte nella sezione Scheda per riempirle automaticamente." }, - "showIdentitiesInVaultView": { - "message": "Mostra le identità come suggerimenti di riempimento automatico nella vista cassaforte" + "showIdentitiesInVaultViewV2": { + "message": "Mostra sempre le identità come suggerimenti di riempimento automatico nella vista cassaforte" }, "showIdentitiesCurrentTab": { "message": "Mostra le identità nella sezione Scheda" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Passa a Premium" }, - "premiumPurchaseAlert": { - "message": "Puoi acquistare il un abbonamento Premium dalla cassaforte web su bitwarden.com. Vuoi visitare il sito?" - }, "premiumPurchaseAlertV2": { "message": "Puoi acquistare Premium dalle impostazioni del tuo account sull'app web Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generatore di nomi utente" }, + "useThisEmail": { + "message": "Usa questa e-mail" + }, "useThisPassword": { "message": "Usa questa password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Molto larga" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aggiornare l'applicazione desktop" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Per usare lo sblocco biometrico, aggiornare l'applicazione desktop o disabilitare lo sblocco dell'impronta digitale nelle impostazioni del desktop." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 1824c9314ee..fb927551b30 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "パスフレーズを生成" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "パスワードの再生成" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "本人確認を行う" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "保管庫がロックされています。続行するには本人確認を行ってください。" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "保管庫にアイテムが見つからない場合は、アイテムを追加するよう要求します。ログインしているすべてのアカウントに適用されます。" }, - "showCardsInVaultView": { - "message": "保管庫ビューに自動入力の候補としてカードを表示する" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "タブページにカードを表示" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "自動入力を簡単にするために、タブページにカードアイテムを表示します" }, - "showIdentitiesInVaultView": { - "message": "保管庫ビューに自動入力の候補として ID を表示する" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "タブページに ID を表示" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "プレミアム会員に加入" }, - "premiumPurchaseAlert": { - "message": "プレミアム会員権は bitwarden.com ウェブ保管庫で購入できます。ウェブサイトを開きますか?" - }, "premiumPurchaseAlertV2": { "message": "Bitwarden ウェブアプリでアカウント設定からプレミアムを購入できます。" }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "ユーザー名生成ツール" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "このパスワードを使用する" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "エクストラワイド" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 1f8f2766e7b..b1c5b7a5e58 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index a7913bbfc9b..eb44a6806d1 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index ce85c7ea820..69d05d03fe6 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಪುನರುತ್ಪಾದಿಸಿ" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಲಾಕ್ ಆಗಿದೆ. ಮುಂದುವರೆಯಲು ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "ಪ್ರೀಮಿಯಂ ಖರೀದಿಸಿ" }, - "premiumPurchaseAlert": { - "message": "ನೀವು ಬಿಟ್ವಾರ್ಡೆನ್.ಕಾಮ್ ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ಪ್ರೀಮಿಯಂ ಸದಸ್ಯತ್ವವನ್ನು ಖರೀದಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index c264492e0d2..93cde61315c 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "암호 생성" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "비밀번호 재생성" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "신원 확인" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "보관함이 잠겨 있습니다. 마스터 비밀번호를 입력하여 계속하세요." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "보관함에 항목이 없을 경우 추가하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다." }, - "showCardsInVaultView": { - "message": "보관함 보기에서 카드 자동 완성 제안을 표시" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "탭 페이지에 카드 표시" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "간편한 자동완성을 위해 탭에 카드 항목들을 나열" }, - "showIdentitiesInVaultView": { - "message": "보관함 보기에서 신원들의 자동 완성 제안을 표시" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "탭 페이지에 신원들을 표시" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "프리미엄 멤버십 구입" }, - "premiumPurchaseAlert": { - "message": "bitwarden.com 웹 보관함에서 프리미엄 멤버십을 구입할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" - }, "premiumPurchaseAlertV2": { "message": "Bitwarden 웹 앱의 계정 설정에서 프리미엄에 대한 결제를 할 수 있습니다." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "사용자 이름 생성기" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "이 비밀번호 사용" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "매우 넓게" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index f76ae921425..bb3e7e2357c 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Generuoti slaptažodį iš naujo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Patvirtinti tapatybę" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Jūsų saugykla užrakinta. Norėdami tęsti, patikrinkite pagrindinį slaptažodį." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Paprašykite pridėti elementą, jei jo nerasta Jūsų saugykloje. Taikoma visoms prisijungusioms paskyroms." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Visada rodyti korteles kaip automatinio pildymo pasiūlymus saugyklos rodinyje" }, "showCardsCurrentTab": { "message": "Rodyti korteles skirtuko puslapyje" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Pateikti kortelių elementų skirtuko puslapyje sąrašą, kad būtų lengva automatiškai užpildyti." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Visada rodyti tapatybes kaip automatinio pildymo pasiūlymus saugyklos rodinyje" }, "showIdentitiesCurrentTab": { "message": "Rodyti tapatybes skirtuko puslapyje" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Įsigyti Premium" }, - "premiumPurchaseAlert": { - "message": "Galite įsigyti „Premium“ narystę „bitwarden.com“ žiniatinklio saugykloje. Ar norite apsilankyti svetainėje dabar?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index d18fbb92039..d256bed25a9 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Izveidot paroles vārdkopu" }, + "passwordGenerated": { + "message": "Parole izveidota" + }, + "passphraseGenerated": { + "message": "Paroles vārdkopa izveidota" + }, + "usernameGenerated": { + "message": "Lietotājvārds izveidots" + }, + "emailGenerated": { + "message": "E-pasta adrese izveidota" + }, "regeneratePassword": { "message": "Pārizveidot paroli" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identitātes apliecināšana" }, + "weDontRecognizeThisDevice": { + "message": "Mēs neatpazīstam šo ierīci. Jāievada kods, kas tika nosūtīts e-pastā, lai apliecinātu savu identitāti." + }, + "continueLoggingIn": { + "message": "Turpināt pieteikšanos" + }, "yourVaultIsLocked": { "message": "Glabātava ir aizslēgta. Jāapliecina sava identitāte, lai turpinātu." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Vaicāt, vai pievienot vienumu, ja glabātavā tāds nav atrodams. Attiecas uz visiem kontiem, kuri ir pieteikušies." }, - "showCardsInVaultView": { - "message": "Rādīt kartes kā automātiskās aizpildes ieteikumus glabātavas skatā" + "showCardsInVaultViewV2": { + "message": "Glabātavas skatā vienmēr rādīt kartes kā automātiskās aizpildes ieteikumus" }, "showCardsCurrentTab": { "message": "Rādīt kartes cilnes lapā" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Attēlot kartes ciļņu lapā vieglākai aizpildīšanai." }, - "showIdentitiesInVaultView": { - "message": "Rādīt identitātes kā automātiskās aizpildes ieteikumus glabātavas skatā" + "showIdentitiesInVaultViewV2": { + "message": "Glabātavas skatā vienmēr rādīt identitātes kā automātiskās aizpildes ieteikumus" }, "showIdentitiesCurrentTab": { "message": "Rādīt identitātes cilnes pārskatā" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Iegādāties Premium" }, - "premiumPurchaseAlert": { - "message": "Premium dalību ir iespējams iegādāties bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" - }, "premiumPurchaseAlertV2": { "message": "Premium var iegādāties Bitwarden tīmekļa lietotnē sava konta iestatījumos." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Lietotājvārdu veidotājs" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Izmantot šo paroli" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Ļoti plats" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lūgums atjaunināt darbvirsmas lietotni" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Lai izmantotu atslēgšanu ar biometriju, lūgums atjaunināt darbvirsmas lietotni vai atspējot atslēgšanu ar pirkstu nospiedumu darbvirsmas iestatījumos." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index cfbb1972388..b9d2858a5c9 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "പാസ്സ്‌വേഡ് വീണ്ടും സൃഷ്ടിക്കുക" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "തങ്ങളുടെ വാൾട് പൂട്ടിയിരിക്കുന്നു. തുടരുന്നതിന് നിങ്ങളുടെ പ്രാഥമിക പാസ്‌വേഡ് പരിശോധിക്കുക." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "പ്രീമിയം വാങ്ങുക" }, - "premiumPurchaseAlert": { - "message": "നിങ്ങൾക്ക് bitwarden.com വെബ് വാൾട്ടിൽ പ്രീമിയം അംഗത്വം വാങ്ങാം. നിങ്ങൾക്ക് ഇപ്പോൾ വെബ്സൈറ്റ് സന്ദർശിക്കാൻ ആഗ്രഹമുണ്ടോ?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 05d6034acec..501f02e4e54 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "पासवर्ड पुनर्जनित करा" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "ओळख सत्यापित करा" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "तुमची तिजोरीला कुलूप लावले आहे. पुढे जाण्यासाठी तुमची ओळख सत्यापित करा." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index a7913bbfc9b..eb44a6806d1 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index df676d2a3ba..101b314c9c2 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -81,10 +81,10 @@ "message": "Et hint for hovedpassordet (valgfritt)" }, "joinOrganization": { - "message": "Join organization" + "message": "Bli med i organisasjonen" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Bli med i $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Omgenerer et passord" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Bekreft identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Hvelvet ditt er låst. Kontroller hovedpassordet ditt for å fortsette." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Vis kort på fanesiden" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Vis kortelementer på fanesiden for lett auto-utfylling." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Vis identiteter på fanesiden" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Kjøp Premium" }, - "premiumPurchaseAlert": { - "message": "Du kan kjøpe et Premium-medlemskap på bitwarden.com. Vil du besøke det nettstedet nå?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Brukernavngenerator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Bruk dette passordet" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra bred" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index a7913bbfc9b..eb44a6806d1 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 8a70ea4548c..46dc3e1166d 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Wachtwoordzin genereren" }, + "passwordGenerated": { + "message": "Wachtwoord gegenereerd" + }, + "passphraseGenerated": { + "message": "Wachtwoorden gegenereerd" + }, + "usernameGenerated": { + "message": "Gebruikersnaam gegenereerd" + }, + "emailGenerated": { + "message": "E-mail gegenereerd" + }, "regeneratePassword": { "message": "Wachtwoord opnieuw genereren" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Identiteit verifiëren" }, + "weDontRecognizeThisDevice": { + "message": "We herkennen dit apparaat niet. Voer de code in die naar je e-mail is verzonden om je identiteit te verifiëren." + }, + "continueLoggingIn": { + "message": "Doorgaan met inloggen" + }, "yourVaultIsLocked": { "message": "Je kluis is vergrendeld. Bevestig je identiteit om door te gaan." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Vraag om een item toe te voegen als het niet is gevonden is je kluis. Dit geld voor alle ingelogde accounts." }, - "showCardsInVaultView": { - "message": "Kaarten als Autofill-suggesties in de kluisweergave weergeven" + "showCardsInVaultViewV2": { + "message": "Kaarten altijd als auto-invullen suggesties in de kluisweergave weergeven" }, "showCardsCurrentTab": { "message": "Kaarten weergeven op tabpagina" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kaartenitems weergeven op de tabpagina voor gemakkelijk automatisch invullen." }, - "showIdentitiesInVaultView": { - "message": "Identiteiten als Autofill-suggesties in de kluisweergave weergeven" + "showIdentitiesInVaultViewV2": { + "message": "Identiteiten altijd als auto-invullen suggesties in de kluisweergave weergeven" }, "showIdentitiesCurrentTab": { "message": "Identiteiten weergeven op tabpagina" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Premium aanschaffen" }, - "premiumPurchaseAlert": { - "message": "Je kunt een Premium-abonnement aanschaffen in de webkluis op bitwarden.com. Wil je de website nu bezoeken?" - }, "premiumPurchaseAlertV2": { "message": "Je kunt Premium via je accountinstellingen in de Bitwarden-webapp kopen." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Gebruikersnaamgenerator" }, + "useThisEmail": { + "message": "Dit e-mailadres gebruiken" + }, "useThisPassword": { "message": "Dit wachtwoord gebruiken" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra breed" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Werk je desktopapplicatie bij" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Als je biometrische gegevens wilt gebruiken, moet je de desktopapplicatie bijwerken of vingerafdrukontgrendeling uitschakelen in de instellingen van de desktopapplicatie." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index a7913bbfc9b..eb44a6806d1 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index a7913bbfc9b..eb44a6806d1 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index b79acf95a1c..8d8f38603c5 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -20,7 +20,7 @@ "message": "Utwórz konto" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nowy użytkownik Bitwarden?" }, "logInWithPasskey": { "message": "Zaloguj się używając passkey" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Wygenruj frazę zabezpieczającą" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Wygeneruj ponownie hasło" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Zweryfikuj tożsamość" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Sejf jest zablokowany. Zweryfikuj swoją tożsamość, aby kontynuować." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Poproś o dodanie elementu, jeśli nie zostanie znaleziony w Twoim sejfie. Dotyczy wszystkich zalogowanych kont." }, - "showCardsInVaultView": { - "message": "Pokaż karty jako sugestie autouzupełniania w widoku sejfu" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Pokaż karty na stronie głównej" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Pokaż elementy karty na stronie głównej, aby ułatwić autouzupełnianie." }, - "showIdentitiesInVaultView": { - "message": "Pokaż tożsamości jako sugestie autouzupełniania w widoku sejfu" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Pokaż tożsamości na stronie głównej" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Kup konto Premium" }, - "premiumPurchaseAlert": { - "message": "Konto Premium możesz zakupić na stronie sejfu bitwarden.com. Czy chcesz otworzyć tę stronę?" - }, "premiumPurchaseAlertV2": { "message": "Możesz kupić Premium w ustawieniach konta w aplikacji internetowej Bitwarden." }, @@ -1320,7 +1335,7 @@ "message": "Limit czasu uwierzytelniania" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "Upłynął limit czasu uwierzytelniania. Uruchom ponownie proces logowania." }, "enterVerificationCodeEmail": { "message": "Wpisz 6-cyfrowy kod weryfikacyjny, który został przesłany na adres $EMAIL$.", @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generator nazw użytkownika" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Użyj tego hasła" }, @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Zablokowane domeny" }, "excludedDomains": { "message": "Wykluczone domeny" @@ -2340,10 +2358,10 @@ "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, "autofillBlockedNoticeV2": { - "message": "Autofill is blocked for this website." + "message": "Autouzupełnianie jest zablokowane dla tej witryny." }, "autofillBlockedNoticeGuidance": { - "message": "Change this in settings" + "message": "Zmień to w ustawieniach" }, "websiteItemLabel": { "message": "Strona internetowa $number$ (URI)", @@ -2364,7 +2382,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Zmiany w zablokowanych domenach zapisane" }, "excludedDomainsSavedSuccess": { "message": "Zmiany w wykluczonych domenach zapisane" @@ -2568,7 +2586,7 @@ "message": "Aby wybrać plik za pomocą przeglądarki Safari, otwórz rozszerzenie w nowym oknie." }, "popOut": { - "message": "Pop out" + "message": "Odepnij" }, "sendFileCalloutHeader": { "message": "Zanim zaczniesz" @@ -2805,10 +2823,10 @@ "message": "Błąd" }, "decryptionError": { - "message": "Decryption error" + "message": "Błąd odszyfrowywania" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden nie mógł odszyfrować elementów sejfu wymienionych poniżej." }, "contactCSToAvoidDataLossPart1": { "message": "Contact customer success", @@ -3109,10 +3127,10 @@ "message": "Powiadomienie zostało wysłane na twoje urządzenie" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Upewnij się, że Twoje konto jest odblokowane, a unikalny identyfikator konta pasuje do drugiego urządzenia" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Zostaniesz powiadomiony po zatwierdzeniu prośby" }, "needAnotherOptionV1": { "message": "Potrzebujesz innego sposobu?" @@ -3976,10 +3994,10 @@ "message": "Passkey został usunięty" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Sugestie autouzupełniania" }, "itemSuggestions": { - "message": "Suggested items" + "message": "Sugerowane elementy" }, "autofillSuggestionsTip": { "message": "Zapisz element logowania dla tej witryny, aby automatycznie wypełnić" @@ -4636,22 +4654,22 @@ "message": "Nie masz uprawnień do edycji tego elementu" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Odblokowanie odciskiem palca jest niedostępne, ponieważ najpierw wymagane jest odblokowanie kodem PIN lub hasłem." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Odblokowanie biometryczne jest obecnie niedostępne." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Odblokowanie biometryczne jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Odblokowanie biometryczne jest niedostępne z powodu nieprawidłowej konfiguracji plików systemowych." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Odblokowanie odciskiem palca jest niedostępne, ponieważ aplikacja desktopowa Bitwarden jest zamknięta." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Odblokowanie biometryczne jest niedostępne, ponieważ nie jest włączone dla $EMAIL$ w aplikacji desktopowej Bitwarden.", "placeholders": { "email": { "content": "$1", @@ -4660,7 +4678,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Odblokowanie biometryczne jest obecnie niedostępne z nieznanego powodu." }, "authenticating": { "message": "Uwierzytelnianie" @@ -4825,22 +4843,22 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Ważna informacja" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Skonfiguruj dwustopniowe logowanie" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden wyśle kod na Twój adres e-mail w celu zweryfikowania logowania z nowych urządzeń, począwszy od lutego 2025 r." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Możesz skonfigurować dwustopniowe logowanie jako alternatywny sposób ochrony konta lub zmienić swój adres e-mail do którego masz dostęp." }, "remindMeLater": { - "message": "Remind me later" + "message": "Przypomnij mi później" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Czy masz pewny dostęp do swojego adresu e-mail, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4849,16 +4867,16 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Nie, nie mam" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Tak, mam pewny dostęp do mojego adresu e-mail" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Włącz dwustopniowe logowanie" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Zmień adres e-mail konta" }, "extensionWidth": { "message": "Szerokość rozszerzenia" @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Bardzo szerokie" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 0971c5e1bc5..2de90042386 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Gerar frase secreta" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Gerar Nova Senha" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar Identidade" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Seu cofre está trancado. Verifique sua identidade para continuar." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Pedir para adicionar um item se um não for encontrado no seu cofre. Aplica-se a todas as contas logadas." }, - "showCardsInVaultView": { - "message": "Mostrar cartões como sugestões de preenchimento automático na exibição do Cofre" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Mostrar cartões em páginas com guias." @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Exibir itens de cartão em páginas com abas para simplificar o preenchimento automático" }, - "showIdentitiesInVaultView": { - "message": "Mostrar identifica como sugestões de preenchimento automático na exibição do Cofre" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Exibir Identidades na Aba Atual" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Comprar Premium" }, - "premiumPurchaseAlert": { - "message": "Você pode comprar a assinatura premium no cofre web em bitwarden.com. Você deseja visitar o site agora?" - }, "premiumPurchaseAlertV2": { "message": "Você pode comprar Premium nas configurações de sua conta no aplicativo web do Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Gerador de usuário" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use esta senha" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra Grande" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 663e337d01c..706e39bff9a 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -62,7 +62,7 @@ "message": "Uma dica da palavra-passe mestra pode ajudá-lo a lembrar-se da sua palavra-passe, caso se esqueça dela." }, "masterPassHintText": { - "message": "Se se esquecer da sua palavra-passe, a dica da palavra-passe pode ser enviada para o seu e-mail. Máximo de $CURRENT$/$MAXIMUM$ caracteres.", + "message": "Se se esquecer da sua palavra-passe, a dica da palavra-passe pode ser enviada para o seu e-mail. Máximo de $CURRENT$/$MAXIMUM$ carateres.", "placeholders": { "current": { "content": "$1", @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Gerar frase de acesso" }, + "passwordGenerated": { + "message": "Palavra-passe gerada" + }, + "passphraseGenerated": { + "message": "Frase de acesso gerada" + }, + "usernameGenerated": { + "message": "Nome de utilizador gerado" + }, + "emailGenerated": { + "message": "E-mail gerado" + }, "regeneratePassword": { "message": "Regenerar palavra-passe" }, @@ -467,7 +479,7 @@ "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caracteres especiais (!@#$%^&*)", + "message": "Carateres especiais (!@#$%^&*)", "description": "deprecated. Use specialCharactersLabel instead." }, "include": { @@ -475,7 +487,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Incluir caracteres em maiúsculas", + "message": "Incluir carateres em maiúsculas", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -483,7 +495,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Incluir caracteres em minúsculas", + "message": "Incluir carateres em minúsculas", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -499,7 +511,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Incluir caracteres especiais", + "message": "Incluir carateres especiais", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -523,10 +535,10 @@ "message": "Mínimo de números" }, "minSpecial": { - "message": "Mínimo de caracteres especiais" + "message": "Mínimo de carateres especiais" }, "avoidAmbiguous": { - "message": "Evitar caracteres ambíguos", + "message": "Evitar carateres ambíguos", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificar identidade" }, + "weDontRecognizeThisDevice": { + "message": "Não reconhecemos este dispositivo. Introduza o código enviado para o seu e-mail para verificar a sua identidade." + }, + "continueLoggingIn": { + "message": "Continuar a iniciar sessão" + }, "yourVaultIsLocked": { "message": "O seu cofre está bloqueado. Verifique a sua identidade para continuar." }, @@ -763,7 +781,7 @@ "message": "É necessário reescrever a palavra-passe mestra." }, "masterPasswordMinlength": { - "message": "A palavra-passe mestra deve ter pelo menos $VALUE$ caracteres.", + "message": "A palavra-passe mestra deve ter pelo menos $VALUE$ carateres.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Pedir para adicionar um item se não for encontrado um no seu cofre. Aplica-se a todas as contas com sessão iniciada." }, - "showCardsInVaultView": { - "message": "Mostrar cartões como sugestões de preenchimento automático na vista do cofre" + "showCardsInVaultViewV2": { + "message": "Mostrar sempre cartões como sugestões de preenchimento automático na vista do cofre" }, "showCardsCurrentTab": { "message": "Mostrar cartões na página Separador" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Listar itens de cartões na página Separador para facilitar o preenchimento automático." }, - "showIdentitiesInVaultView": { - "message": "Mostrar identidades como sugestões de preenchimento automático na vista do cofre" + "showIdentitiesInVaultViewV2": { + "message": "Mostrar sempre identidades como sugestões de preenchimento automático na vista do cofre" }, "showIdentitiesCurrentTab": { "message": "Mostrar identidades na página Separador" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Adquirir Premium" }, - "premiumPurchaseAlert": { - "message": "Pode adquirir uma subscrição Premium no cofre web em bitwarden.com. Pretende visitar o site agora?" - }, "premiumPurchaseAlertV2": { "message": "Pode adquirir o Premium a partir das definições da sua conta na aplicação Web Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Gerador de nomes de utilizador" }, + "useThisEmail": { + "message": "Utilizar este e-mail" + }, "useThisPassword": { "message": "Utilizar esta palavra-passe" }, @@ -2168,16 +2186,16 @@ } }, "policyInEffectUppercase": { - "message": "Contém um ou mais caracteres em maiúsculas" + "message": "Contém um ou mais carateres em maiúsculas" }, "policyInEffectLowercase": { - "message": "Contém um ou mais caracteres em minúsculas" + "message": "Contém um ou mais carateres em minúsculas" }, "policyInEffectNumbers": { "message": "Contém um ou mais números" }, "policyInEffectSpecial": { - "message": "Contém um ou mais dos seguintes caracteres especiais $CHARS$", + "message": "Contém um ou mais dos seguintes carateres especiais $CHARS$", "placeholders": { "chars": { "content": "$1", @@ -2772,7 +2790,7 @@ "message": "Saiu da organização." }, "toggleCharacterCount": { - "message": "Mostrar/ocultar contagem de caracteres" + "message": "Mostrar/ocultar contagem de carateres" }, "sessionTimeout": { "message": "A sua sessão expirou. Por favor, volte atrás e tente iniciar sessão novamente." @@ -2839,7 +2857,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Utilize $RECOMMENDED$ caracteres ou mais para gerar uma palavra-passe forte.", + "message": " Utilize $RECOMMENDED$ carateres ou mais para gerar uma palavra-passe forte.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3142,7 +3160,7 @@ "message": "A sua palavra-passe mestra não pode ser recuperada se a esquecer!" }, "characterMinimum": { - "message": "$LENGTH$ caracteres no mínimo", + "message": "$LENGTH$ carateres no mínimo", "placeholders": { "length": { "content": "$1", @@ -3319,7 +3337,7 @@ "message": "Procurar" }, "inputMinLength": { - "message": "O campo deve ter pelo menos $COUNT$ caracteres.", + "message": "O campo deve ter pelo menos $COUNT$ carateres.", "placeholders": { "count": { "content": "$1", @@ -3328,7 +3346,7 @@ } }, "inputMaxLength": { - "message": "O campo não pode exceder os $COUNT$ caracteres de comprimento.", + "message": "O campo não pode exceder os $COUNT$ carateres de comprimento.", "placeholders": { "count": { "content": "$1", @@ -3337,7 +3355,7 @@ } }, "inputForbiddenCharacters": { - "message": "Não são permitidos os seguintes caracteres: $CHARACTERS$", + "message": "Não são permitidos os seguintes carateres: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -3346,7 +3364,7 @@ } }, "inputMinValue": { - "message": "O valor do campo tem de ser, pelo menos, $MIN$ caracteres.", + "message": "O valor do campo tem de ser, pelo menos, $MIN$ carateres.", "placeholders": { "min": { "content": "$1", @@ -3355,7 +3373,7 @@ } }, "inputMaxValue": { - "message": "O valor do campo não pode exceder os $MAX$ caracteres.", + "message": "O valor do campo não pode exceder os $MAX$ carateres.", "placeholders": { "max": { "content": "$1", @@ -4078,7 +4096,7 @@ "message": "Notificações" }, "appearance": { - "message": "Aparência" + "message": "Aspeto" }, "errorAssigningTargetCollection": { "message": "Erro ao atribuir a coleção de destino." @@ -4609,10 +4627,10 @@ "message": "Ficheiro guardado no dispositivo. Gira-o a partir das transferências do seu dispositivo." }, "showCharacterCount": { - "message": "Mostrar contagem de caracteres" + "message": "Mostrar contagem de carateres" }, "hideCharacterCount": { - "message": "Ocultar contagem de caracteres" + "message": "Ocultar contagem de carateres" }, "itemsInTrash": { "message": "Itens no lixo" @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Muito ampla" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Por favor, atualize a sua aplicação para computador" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Para utilizar o desbloqueio biométrico, atualize a sua aplicação para computador ou desative o desbloqueio por impressão digital nas definições dessa mesma app." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 165c3749b53..966d2b4e01d 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerare parolă" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verificare identitate" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Seiful dvs. este blocat. Verificați-vă identitatea pentru a continua." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Afișați cardurile pe pagina Filă" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Listați elementele cardului pe pagina Filă pentru a facilita completarea automată." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Afișați identitățile pe pagina Filă" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Achiziționare abonament Premium" }, - "premiumPurchaseAlert": { - "message": "Puteți achiziționa un abonament Premium pe website-ul bitwarden.com. Doriți să vizitați site-ul acum?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 5519737f16c..f0e3b53bfb2 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Создать парольную фразу" }, + "passwordGenerated": { + "message": "Пароль создан" + }, + "passphraseGenerated": { + "message": "Парольная фраза создана" + }, + "usernameGenerated": { + "message": "Имя пользователя создано" + }, + "emailGenerated": { + "message": "Email создан" + }, "regeneratePassword": { "message": "Создать новый пароль" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Подтвердить личность" }, + "weDontRecognizeThisDevice": { + "message": "Мы не распознали это устройство. Введите код, отправленный на ваш email, чтобы подтвердить вашу личность." + }, + "continueLoggingIn": { + "message": "Продолжить вход" + }, "yourVaultIsLocked": { "message": "Ваше хранилище заблокировано. Подтвердите свою личность, чтобы продолжить" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Запрос на добавление элемента, если он отсутствует в вашем хранилище. Применяется ко всем авторизованным аккаунтам." }, - "showCardsInVaultView": { - "message": "Показывать карты как предложение автозаполнения при просмотре Хранилище" + "showCardsInVaultViewV2": { + "message": "Всегда показывать карты как предложения автозаполнения при просмотре хранилища" }, "showCardsCurrentTab": { "message": "Показывать карты на вкладке" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Карты будут отображены на вкладке для удобного автозаполнения." }, - "showIdentitiesInVaultView": { - "message": "Показывать личности как предложение автозаполнения при просмотре Хранилище" + "showIdentitiesInVaultViewV2": { + "message": "Всегда показывать личности как предложения автозаполнения при просмотре хранилища" }, "showIdentitiesCurrentTab": { "message": "Показывать Личности на вкладке" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Купить Премиум" }, - "premiumPurchaseAlert": { - "message": "Вы можете купить Премиум на bitwarden.com. Перейти на сайт сейчас?" - }, "premiumPurchaseAlertV2": { "message": "Премиум можно приобрести в настройках аккаунта в веб-версии Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Генератор имени пользователя" }, + "useThisEmail": { + "message": "Использовать этот email" + }, "useThisPassword": { "message": "Использовать этот пароль" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Очень широкое" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Пожалуйста, обновите приложение для компьютера" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Чтобы использовать биометрическую разблокировку, обновите приложение для компьютера или отключите разблокировку по отпечатку пальца в настройках компьютера." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 801e088aca8..9bd2006f1b2 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "මුරපදය ප්රතිජනනය" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "අනන්යතාවය සත්යාපනය කරන්න" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ඔබේ සුරක්ෂිතාගාරය අගුළු දමා ඇත. දිගටම කරගෙන යාමට ඔබේ අනන්යතාවය සත්යාපනය කරන්න." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "වාරික මිලදී" }, - "premiumPurchaseAlert": { - "message": "ඔබට bitwarden.com වෙබ් සුරක්ෂිතාගාරයේ වාරික සාමාජිකත්වය මිලදී ගත හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 92d0e35a51c..d8bbe1645fa 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generovať prístupovú frázu" }, + "passwordGenerated": { + "message": "Heslo vygenerované" + }, + "passphraseGenerated": { + "message": "Prístupová fráza vygenerovaná" + }, + "usernameGenerated": { + "message": "Používateľské meno vygenerované" + }, + "emailGenerated": { + "message": "E-mail vygenoravný" + }, "regeneratePassword": { "message": "Vygenerovať nové heslo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Overiť identitu" }, + "weDontRecognizeThisDevice": { + "message": "Toto zariadenie nepoznáme. Na overenie vašej totožnosti zadajte kód, ktorý bol zaslaný na váš e-mail." + }, + "continueLoggingIn": { + "message": "Pokračovať v prihlasovaní" + }, "yourVaultIsLocked": { "message": "Váš trezor je uzamknutý. Ak chcete pokračovať, overte svoju identitu." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Požiada o pridanie položky, ak sa v trezore nenachádza. Platí pre všetky prihlásené účty." }, - "showCardsInVaultView": { - "message": "Zobraziť karty ako návrhy automatického vypĺňania v zobrazení trezora" + "showCardsInVaultViewV2": { + "message": "Vždy zobraziť karty ako návrhy automatického vypĺňania v zobrazení trezora" }, "showCardsCurrentTab": { "message": "Zobraziť karty na stránke \"Aktuálna karta\"" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Zoznam položiek karty na stránke \"Aktuálna karta\" na jednoduché automatické vyplnenie." }, - "showIdentitiesInVaultView": { - "message": "Zobraziť identity ako návrhy automatického vypĺňania v zobrazení trezora" + "showIdentitiesInVaultViewV2": { + "message": "Vždy zobraziť identity ako návrhy automatického vypĺňania v zobrazení trezora" }, "showIdentitiesCurrentTab": { "message": "Zobraziť identity na stránke \"Aktuálna karta\"" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Zakúpiť Prémiový účet" }, - "premiumPurchaseAlert": { - "message": "Svoje prémiové členstvo si môžete zakúpiť vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" - }, "premiumPurchaseAlertV2": { "message": "Prémiové členstvo si môžete zakúpiť v nastaveniach svojho účtu vo webovej aplikácii Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Generátor používateľského mena" }, + "useThisEmail": { + "message": "Použiť tento e-mail" + }, "useThisPassword": { "message": "Použiť toto heslo" }, @@ -4840,7 +4858,7 @@ "message": "Pripomenúť neskôr" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Máte spoľahlivý prístup k svojmu e-mailu, $EMAIL$?", + "message": "Máte zaručený prístup k e-mailu $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4852,7 +4870,7 @@ "message": "Nie, nemám" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Áno, mám spoľahlivý prístup k svojmu e-mailu" + "message": "Áno, mám zaručený prístup k e-mailu" }, "turnOnTwoStepLogin": { "message": "Zapnúť dvojstupňové prihlásenie" @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra široké" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Aktualizujte desktopovú aplikáciu" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Ak chcete používať biometrické odomykanie, aktualizujte desktopovú aplikáciu alebo vypnite odomykanie odtlačkom prsta v nastaveniach desktopovej aplikácie." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index d57d6ec5e57..d533c3e02dc 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Ponovno ustvari geslo" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Preverjanje istovetnosti" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Vaš trezor je zaklenjen. Za nadaljevanje potrdite svojo identiteto." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Če predmeta ni v trezorju, ga je potrebno dodati. Velja za vse prijavljene račune." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Prikaži kartice na strani Zavihek" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Na strani Zavihek prikaži kartice za lažje samodejno izpoljnjevanje." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Prikaži identitete na strani Zavihek" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Kupite premium članstvo" }, - "premiumPurchaseAlert": { - "message": "Premium članstvo lahko kupite na spletnem trezoju bitwarden.com. Želite obiskati spletno stran zdaj?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 80de0ba3c8e..3cf637b21f1 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -159,7 +159,7 @@ "message": "Копирај јавни кључ" }, "copyFingerprint": { - "message": "Копирати отисак" + "message": "Копирај отисак прста" }, "copyCustomField": { "message": "Копирати $FIELD$", @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Генеришите приступну фразу" }, + "passwordGenerated": { + "message": "Лозинка генерисана" + }, + "passphraseGenerated": { + "message": "Приступна фраза је генерисана" + }, + "usernameGenerated": { + "message": "Корисничко име генерисано" + }, + "emailGenerated": { + "message": "Имејл генерисан" + }, "regeneratePassword": { "message": "Поново генериши лозинку" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Потврдите идентитет" }, + "weDontRecognizeThisDevice": { + "message": "Не препознајемо овај уређај. Унесите код послат на адресу ваше електронске поште да би сте потврдили ваш идентитет." + }, + "continueLoggingIn": { + "message": "Настави са пријављивањем" + }, "yourVaultIsLocked": { "message": "Сеф је закључан. Унесите главну лозинку за наставак." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Затражите да додате ставку ако она није пронађена у вашем сефу. Односи се на све пријављене налоге." }, - "showCardsInVaultView": { - "message": "Прикажите картице као предлоге за ауто-попуњавање у приказу сефа" + "showCardsInVaultViewV2": { + "message": "Увек приказуј картице као препоруке аутоматског попуњавања на приказу трезора" }, "showCardsCurrentTab": { "message": "Прикажи кредитне картице на страници картице" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Прикажи ставке кредитних картица на страници картице за лакше аутоматско допуњавање." }, - "showIdentitiesInVaultView": { - "message": "Прикажите идентитете као предлоге за ауто-попуњавање у приказу сефа" + "showIdentitiesInVaultViewV2": { + "message": "Увек приказуј идентитете као препоруке аутоматског попуњавања на приказу трезора" }, "showIdentitiesCurrentTab": { "message": "Прикажи идентитете на страници" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Купити премијум" }, - "premiumPurchaseAlert": { - "message": "Можете купити премијум претплату на bitwarden.com. Да ли желите да посетите веб сајт сада?" - }, "premiumPurchaseAlertV2": { "message": "Можете да купите Премиум у подешавањима налога у веб апликацији Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Генератор корисничког имена" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Употреби ову лозинку" }, @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "Блокирани домени" }, "excludedDomains": { "message": "Изузети домени" @@ -2337,13 +2355,13 @@ "message": "Bitwarden неће тражити да сачува податке за пријављивање за ове домене за све пријављене налоге. Морате освежити страницу да би промене ступиле на снагу." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "Аутоматско попуњавање и сродне функције неће бити понуђене за ове веб сајтове. Морате освежити страницу да би се измене примениле." }, "autofillBlockedNoticeV2": { - "message": "Autofill is blocked for this website." + "message": "Аутоматско попуњавање је блокирано за овај веб сајт." }, "autofillBlockedNoticeGuidance": { - "message": "Change this in settings" + "message": "Промените ово у подешавањима" }, "websiteItemLabel": { "message": "Сајт $number$ (УРЛ)", @@ -2364,7 +2382,7 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "Измене блокираних домена су сачуване" }, "excludedDomainsSavedSuccess": { "message": "Изузете промене домена су сачуване" @@ -2805,17 +2823,17 @@ "message": "Грешка" }, "decryptionError": { - "message": "Decryption error" + "message": "Грешка при декрипцији" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden није могао да декриптује ставке из трезора наведене испод." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Обратите се корисничкој подршци", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "да бисте избегли додатни губитак података.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { @@ -3976,10 +3994,10 @@ "message": "Приступни кључ је уклоњен" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Предлози аутоматског попуњавања" }, "itemSuggestions": { - "message": "Suggested items" + "message": "Предложене ставке" }, "autofillSuggestionsTip": { "message": "Сачувајте ставку за пријаву за ову локацију за ауто-попуњавање" @@ -4636,22 +4654,22 @@ "message": "Немате дозволу да уређујете ову ставку" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "Биометријско откључавање није доступно јер је пре тога потребно унети ПИН или лозинку за откључавање." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "Биометријско откључавање тренутно није доступно." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Биометријско откључавање није доступно због лоше подешених системских датотека." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "Биометријско откључавање није доступно због лоше подешених системских датотека." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "Биометријско откључавање није доступно јер је Bitwarden апликација на рачунару угашена." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "Биометријско откључавање није доступно јер није омогућено за $EMAIL$ у Bitwarden апликацији на рачунару.", "placeholders": { "email": { "content": "$1", @@ -4660,7 +4678,7 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "Биометријско откључавање није доступно из непознатог разлога." }, "authenticating": { "message": "Аутентификација" @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Врло широко" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Молим вас надоградите вашу апликацију на рачунару" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Да би сте користили биометријско откључавање, надоградите вашу апликацију на рачунару, или онемогућите откључавање отиском прста у подешавањима на рачунару." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 1927f12bc27..c999454fa9d 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generera lösenfras" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Återskapa lösenord" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verifiera identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Ditt valv är låst. Verifiera din identitet för att fortsätta." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Visa kort på fliksida" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Lista kortobjekt på fliksidan för enkel automatisk fyllning." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Visa identiteter på fliksidan" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Köp Premium" }, - "premiumPurchaseAlert": { - "message": "Du kan köpa premium-medlemskap i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index a7913bbfc9b..eb44a6806d1 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Verify identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Show cards on Tab page" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy autofill." }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index a8a2585f42f..991fc80f6d6 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Regenerate Password" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "ยืนยันตัวตน" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "ตู้เซฟของคุณถูกล็อก ยืนยันตัวตนของคุณเพื่อดำเนินการต่อ" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, - "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "แสดงการ์ดบนหน้าแท็บ" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "บัตรรายการในหน้าแท็บเพื่อให้ป้อนอัตโนมัติได้ง่าย" }, - "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "แสดงตัวตนบนหน้าแท็บ" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Purchase Premium" }, - "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" - }, "premiumPurchaseAlertV2": { "message": "You can purchase Premium from your account settings on the Bitwarden web app." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Username generator" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 34ad0f14483..e54944e8222 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Parola üret" }, + "passwordGenerated": { + "message": "Parola üretildi" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Kullanıcı adı üretildi" + }, + "emailGenerated": { + "message": "E-posta üretildi" + }, "regeneratePassword": { "message": "Yeni parola oluştur" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Kimliği doğrula" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanıyamadık. Kimliğinizi doğrulamak için e-postanıza gönderilen kodu girin." + }, + "continueLoggingIn": { + "message": "Giriş yapmaya devam et" + }, "yourVaultIsLocked": { "message": "Kasanız kilitli. Devam etmek için kimliğinizi doğrulayın." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Kasanızda bulunmayan kayıtların eklenmesini isteyip istemediğinizi sorar. Oturum açmış tüm hesaplar için geçerlidir." }, - "showCardsInVaultView": { - "message": "Kasa görünümünde kartları otomatik doldurma önerisi olarak göster" + "showCardsInVaultViewV2": { + "message": "Kasa görünümünde kartları her zaman otomatik doldurma önerisi olarak göster" }, "showCardsCurrentTab": { "message": "Sekme sayfasında kartları göster" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Kolay otomatik doldurma için sekme sayfasında kartları listele." }, - "showIdentitiesInVaultView": { - "message": "Kasa görünümünde kimlikleri otomatik doldurma önerisi olarak göster" + "showIdentitiesInVaultViewV2": { + "message": "Kasa görünümünde kimlikleri her zaman otomatik doldurma önerisi olarak göster" }, "showIdentitiesCurrentTab": { "message": "Sekme sayfasında kimlikleri göster" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Premium satın al" }, - "premiumPurchaseAlert": { - "message": "Premium üyeliği bitwarden.com web kasası üzerinden satın alabilirsiniz. Şimdi siteye gitmek ister misiniz?" - }, "premiumPurchaseAlertV2": { "message": "Bitwarden web uygulamasındaki hesap ayarlarınızdan Premium abonelik satın alabilirsiniz." }, @@ -1466,7 +1481,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Önerileri otomatik doldur" + "message": "Otomatik doldurma önerileri" }, "showInlineMenuLabel": { "message": "Form alanlarında otomatik doldurma önerilerini göster" @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Kullanıcı adı üreteci" }, + "useThisEmail": { + "message": "Bu e-postayı kullan" + }, "useThisPassword": { "message": "Bu parolayı kullan" }, @@ -3976,7 +3994,7 @@ "message": "Geçiş anahtarı kaldırıldı" }, "autofillSuggestions": { - "message": "Önerileri otomatik doldur" + "message": "Otomatik doldurma önerileri" }, "itemSuggestions": { "message": "Önerilen kayıtlar" @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Ekstra geniş" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Lütfen masaüstü uygulamanızı güncelleyin" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index a1ec52ee80e..dfc8f700352 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Генерувати парольну фразу" }, + "passwordGenerated": { + "message": "Пароль згенеровано" + }, + "passphraseGenerated": { + "message": "Парольну фразу згенеровано" + }, + "usernameGenerated": { + "message": "Ім'я користувача згенеровано" + }, + "emailGenerated": { + "message": "Адресу е-пошти згенеровано" + }, "regeneratePassword": { "message": "Генерувати новий" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Виконати перевірку" }, + "weDontRecognizeThisDevice": { + "message": "Ми не розпізнаємо цей пристрій. Введіть код, надісланий на вашу електронну пошту, щоб підтвердити вашу особу." + }, + "continueLoggingIn": { + "message": "Продовжити вхід" + }, "yourVaultIsLocked": { "message": "Ваше сховище заблоковане. Для продовження виконайте перевірку." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Запитувати про додавання запису, якщо такого не знайдено у вашому сховищі. Застосовується для всіх облікових записів, до яких виконано вхід." }, - "showCardsInVaultView": { - "message": "Показувати картки як пропозиції автозаповнення в режимі перегляду сховища" + "showCardsInVaultViewV2": { + "message": "Завжди показувати картки як пропозиції автозаповнення в режимі перегляду сховища" }, "showCardsCurrentTab": { "message": "Показувати картки на вкладці" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Показувати список карток на сторінці вкладки для легкого автозаповнення." }, - "showIdentitiesInVaultView": { - "message": "Показувати посвідчення як пропозиції автозаповнення в режимі перегляду сховища" + "showIdentitiesInVaultViewV2": { + "message": "Завжди показувати посвідчення як пропозиції автозаповнення в режимі перегляду сховища" }, "showIdentitiesCurrentTab": { "message": "Показувати посвідчення на вкладці" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Придбати преміум" }, - "premiumPurchaseAlert": { - "message": "Ви можете передплатити преміум у сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" - }, "premiumPurchaseAlertV2": { "message": "Ви можете придбати Преміум у налаштуваннях облікового запису вебпрограмі Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Генератор імені користувача" }, + "useThisEmail": { + "message": "Використати цю е-пошту" + }, "useThisPassword": { "message": "Використати цей пароль" }, @@ -2391,7 +2409,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Надіслати подробиці", + "message": "Подробиці відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Дуже широке" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Оновіть свою комп'ютерну програму" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "Щоб використовувати біометричне розблокування, оновіть комп'ютерну програму, або вимкніть розблокування відбитком пальця в налаштуваннях системи." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index b6668d8dc99..9394ebdc8d7 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "Generate passphrase" }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "Tạo lại mật khẩu" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "Xác minh danh tính" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "yourVaultIsLocked": { "message": "Kho của bạn đã bị khóa. Xác minh danh tính của bạn để mở khoá." }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, - "showCardsInVaultView": { - "message": "Hiển thị các thẻ như các gợi ý tự động điền trên giao diện kho" + "showCardsInVaultViewV2": { + "message": "Always show cards as Autofill suggestions on Vault view" }, "showCardsCurrentTab": { "message": "Hiển thị thẻ trên trang Tab" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "Liệt kê các mục thẻ trên trang Tab để dễ dàng tự động điền." }, - "showIdentitiesInVaultView": { - "message": "Hiển thị các danh tính như các gợi ý tự động điền trên giao diện kho" + "showIdentitiesInVaultViewV2": { + "message": "Always show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Hiển thị danh tính trên trang Tab" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "Mua bản Cao Cấp" }, - "premiumPurchaseAlert": { - "message": "Bạn có thể nâng cấp làm thành viên cao cấp trong kho bitwarden nền web. Bạn có muốn truy cập trang web bây giờ?" - }, "premiumPurchaseAlertV2": { "message": "Bạn có thể mua gói Premium từ cài đặt tài khoản trên trang Bitwarden." }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "Bộ tạo tên người dùng" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "Use this password" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 8cd91d44a8a..cc0cc7b8bd2 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -56,7 +56,7 @@ "message": "主密码" }, "masterPassDesc": { - "message": "主密码是您访问密码库的唯一密码。它非常重要,请您不要忘记。一旦忘记,无任何办法恢复此密码。" + "message": "主密码是用于访问您的密码库的密码。不要忘记您的主密码,这一点非常重要。一旦忘记,无任何办法恢复此密码。" }, "masterPassHintDesc": { "message": "主密码提示可以在您忘记密码时帮您回忆起来。" @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "生成密码短语" }, + "passwordGenerated": { + "message": "密码已生成" + }, + "passphraseGenerated": { + "message": "密码短语已生成" + }, + "usernameGenerated": { + "message": "用户名已生成" + }, + "emailGenerated": { + "message": "电子邮箱已生成" + }, "regeneratePassword": { "message": "重新生成密码" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "验证身份" }, + "weDontRecognizeThisDevice": { + "message": "我们无法识别这个设备。请输入发送到您电子邮箱中的代码以验证您的身份。" + }, + "continueLoggingIn": { + "message": "继续登录" + }, "yourVaultIsLocked": { "message": "您的密码库已锁定。请先验证您的身份。" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "如果在密码库中找不到项目,询问添加一个。适用于所有已登录的账户。" }, - "showCardsInVaultView": { - "message": "在密码库视图中将支付卡显示为自动填充建议" + "showCardsInVaultViewV2": { + "message": "在密码库视图中将支付卡始终显示为自动填充建议" }, "showCardsCurrentTab": { "message": "在标签页上显示支付卡" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "在标签页上列出支付卡项目,以便于自动填充。" }, - "showIdentitiesInVaultView": { - "message": "在密码库视图中将身份显示为自动填充建议" + "showIdentitiesInVaultViewV2": { + "message": "在密码库视图中将身份始终显示为自动填充建议" }, "showIdentitiesCurrentTab": { "message": "在标签页上显示身份" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "购买高级版" }, - "premiumPurchaseAlert": { - "message": "您可以在 bitwarden.com 网页版密码库购买高级会员。现在要访问吗?" - }, "premiumPurchaseAlertV2": { "message": "您可以在 Bitwarden 网页 App 的账户设置中购买高级版。" }, @@ -1744,7 +1759,7 @@ "message": "州 / 省" }, "zipPostalCode": { - "message": "邮编 / 邮政代码" + "message": "邮政编码" }, "country": { "message": "国家" @@ -1856,7 +1871,7 @@ "message": "检查密码是否已经被公开。" }, "passwordExposed": { - "message": "此密码在泄露数据中已被公开 $VALUE$ 次。请立即修改。", + "message": "此密码在数据泄露中已被暴露 $VALUE$ 次。请立即修改。", "placeholders": { "value": { "content": "$1", @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "用户名生成器" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "使用此密码" }, @@ -2325,7 +2343,7 @@ "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "屏蔽域名" + "message": "屏蔽的域名" }, "excludedDomains": { "message": "排除域名" @@ -2488,7 +2506,7 @@ "message": "自定义" }, "sendPasswordDescV3": { - "message": "添加一个用于收件人访问此 Send 的可选密码。", + "message": "添加一个用于接收者访问此 Send 的可选密码。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "超宽" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "请更新您的桌面应用程序" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "要使用生物识别解锁,请更新您的桌面应用程序,或在桌面设置中禁用指纹解锁。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 13edf4920de..f9ef4d56a49 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -445,6 +445,18 @@ "generatePassphrase": { "message": "產生密碼短語" }, + "passwordGenerated": { + "message": "已產生密碼" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "regeneratePassword": { "message": "重新產生密碼" }, @@ -647,6 +659,12 @@ "verifyIdentity": { "message": "驗證身份" }, + "weDontRecognizeThisDevice": { + "message": "我們無法識別此裝置。請輸入已傳送到您電子郵件的驗證碼以驗證您的身分。" + }, + "continueLoggingIn": { + "message": "繼續登入" + }, "yourVaultIsLocked": { "message": "您的密碼庫已鎖定。請驗證身分以繼續。" }, @@ -986,8 +1004,8 @@ "addLoginNotificationDescAlt": { "message": "如果在您的密碼庫中找不到項目,則詢問是否新增項目。適用於所有已登入的帳戶。" }, - "showCardsInVaultView": { - "message": "在密碼庫介面中顯示支付卡自動填入建議" + "showCardsInVaultViewV2": { + "message": "一律在密碼庫介面中顯示支付卡自動填入建議" }, "showCardsCurrentTab": { "message": "於分頁頁面顯示支付卡" @@ -995,8 +1013,8 @@ "showCardsCurrentTabDesc": { "message": "於分頁頁面顯示信用卡以便於自動填入。" }, - "showIdentitiesInVaultView": { - "message": "在密碼庫介面中顯示身分自動填入建議" + "showIdentitiesInVaultViewV2": { + "message": "一律在密碼庫介面中顯示身分自動填入建議" }, "showIdentitiesCurrentTab": { "message": "於分頁頁面顯示身分" @@ -1262,9 +1280,6 @@ "premiumPurchase": { "message": "升級為進階會員" }, - "premiumPurchaseAlert": { - "message": "您可以在 bitwarden.com 網頁版密碼庫購買進階會員資格。現在要前往嗎?" - }, "premiumPurchaseAlertV2": { "message": "您可以在 Bitwarden 網頁 App 的帳號設定中購買進階版。" }, @@ -2046,6 +2061,9 @@ "usernameGenerator": { "message": "使用者名稱產生器" }, + "useThisEmail": { + "message": "Use this email" + }, "useThisPassword": { "message": "使用此密碼" }, @@ -4868,5 +4886,11 @@ }, "extraWide": { "message": "更寬" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "請更新您的桌面應用程式" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "為了使用生物辨識解鎖,請更新您的桌面應用程式,或在設定中停用指紋解鎖。" } } diff --git a/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html index e7fafbb252c..34c0cbe9614 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html +++ b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html @@ -7,13 +7,20 @@
-

{{ "loginInitiated" | i18n }}

+

{{ "logInRequestSent" | i18n }}

-

{{ "notificationSentDevice" | i18n }}

-

- {{ "fingerprintMatchInfo" | i18n }} + {{ "notificationSentDevicePart1" | i18n }} + {{ "notificationSentDeviceAnchor" | i18n }}. {{ "notificationSentDevicePart2" | i18n }}

diff --git a/apps/browser/src/auth/popup/register.component.ts b/apps/browser/src/auth/popup/register.component.ts index f8a332f0fd1..50475b2204d 100644 --- a/apps/browser/src/auth/popup/register.component.ts +++ b/apps/browser/src/auth/popup/register.component.ts @@ -13,9 +13,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KeyService } from "@bitwarden/key-management"; @Component({ @@ -34,9 +32,7 @@ export class RegisterComponent extends BaseRegisterComponent { i18nService: I18nService, keyService: KeyService, apiService: ApiService, - stateService: StateService, platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationServiceAbstraction, environmentService: EnvironmentService, logService: LogService, auditService: AuditService, @@ -51,9 +47,7 @@ export class RegisterComponent extends BaseRegisterComponent { i18nService, keyService, apiService, - stateService, platformUtilsService, - passwordGenerationService, environmentService, logService, auditService, diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index a2f9cd9d0fc..43230bd23f4 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -33,7 +33,7 @@ import { BrowserApi } from "../../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; -import { closeTwoFactorAuthPopout } from "./utils/auth-popout-window"; +import { closeTwoFactorAuthWebAuthnPopout } from "./utils/auth-popout-window"; @Component({ selector: "app-two-factor", @@ -171,7 +171,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit // We don't need this window anymore because the intent is for the user to be left // on the web vault screen which tells them to continue in the browser extension (sidebar or popup) - await closeTwoFactorAuthPopout(); + await closeTwoFactorAuthWebAuthnPopout(); }; } }); diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts index 9e7d69fad97..deb71f73cd6 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts @@ -7,8 +7,9 @@ import { openUnlockPopout, closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, - closeTwoFactorAuthPopout, + openTwoFactorAuthWebAuthnPopout, + closeTwoFactorAuthWebAuthnPopout, + closeSsoAuthResultPopout, } from "./auth-popout-window"; describe("AuthPopoutWindow", () => { @@ -97,22 +98,30 @@ describe("AuthPopoutWindow", () => { }); }); - describe("openTwoFactorAuthPopout", () => { - it("opens a window that facilitates two factor authentication", async () => { - await openTwoFactorAuthPopout({ data: "data", remember: "remember" }); + describe("closeSsoAuthResultPopout", () => { + it("closes the SSO authentication result popout window", async () => { + await closeSsoAuthResultPopout(); + + expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.ssoAuthResult); + }); + }); + + describe("openTwoFactorAuthWebAuthnPopout", () => { + it("opens a window that facilitates two factor authentication via WebAuthn", async () => { + await openTwoFactorAuthWebAuthnPopout({ data: "data", remember: "remember" }); expect(openPopoutSpy).toHaveBeenCalledWith( "popup/index.html#/2fa;webAuthnResponse=data;remember=remember", - { singleActionKey: AuthPopoutType.twoFactorAuth }, + { singleActionKey: AuthPopoutType.twoFactorAuthWebAuthn }, ); }); }); - describe("closeTwoFactorAuthPopout", () => { - it("closes the two-factor authentication window", async () => { - await closeTwoFactorAuthPopout(); + describe("closeTwoFactorAuthWebAuthnPopout", () => { + it("closes the two-factor authentication WebAuthn window", async () => { + await closeTwoFactorAuthWebAuthnPopout(); - expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.twoFactorAuth); + expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(AuthPopoutType.twoFactorAuthWebAuthn); }); }); }); diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.ts index 5a0e577807f..8d6e7fa92cd 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.ts @@ -6,7 +6,7 @@ import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; const AuthPopoutType = { unlockExtension: "auth_unlockExtension", ssoAuthResult: "auth_ssoAuthResult", - twoFactorAuth: "auth_twoFactorAuth", + twoFactorAuthWebAuthn: "auth_twoFactorAuthWebAuthn", } as const; const extensionUnlockUrls = new Set([ chrome.runtime.getURL("popup/index.html#/lock"), @@ -60,26 +60,37 @@ async function openSsoAuthResultPopout(resultData: { code: string; state: string } /** - * Opens a window that facilitates two-factor authentication. - * - * @param twoFactorAuthData - The data from the two-factor authentication. + * Closes the SSO authentication result popout window. */ -async function openTwoFactorAuthPopout(twoFactorAuthData: { data: string; remember: string }) { - const { data, remember } = twoFactorAuthData; +async function closeSsoAuthResultPopout() { + await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.ssoAuthResult); +} + +/** + * Opens a popout that facilitates two-factor authentication via WebAuthn. + * + * @param twoFactorAuthWebAuthnData - The data to send ot the popout via query param. + * It includes the WebAuthn response and whether to save the 2FA remember me token or not. + */ +async function openTwoFactorAuthWebAuthnPopout(twoFactorAuthWebAuthnData: { + data: string; + remember: string; +}) { + const { data, remember } = twoFactorAuthWebAuthnData; const params = `webAuthnResponse=${encodeURIComponent(data)};` + `remember=${encodeURIComponent(remember)}`; const twoFactorUrl = `popup/index.html#/2fa;${params}`; await BrowserPopupUtils.openPopout(twoFactorUrl, { - singleActionKey: AuthPopoutType.twoFactorAuth, + singleActionKey: AuthPopoutType.twoFactorAuthWebAuthn, }); } /** * Closes the two-factor authentication popout window. */ -async function closeTwoFactorAuthPopout() { - await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.twoFactorAuth); +async function closeTwoFactorAuthWebAuthnPopout() { + await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.twoFactorAuthWebAuthn); } export { @@ -87,6 +98,7 @@ export { openUnlockPopout, closeUnlockPopout, openSsoAuthResultPopout, - openTwoFactorAuthPopout, - closeTwoFactorAuthPopout, + closeSsoAuthResultPopout, + openTwoFactorAuthWebAuthnPopout, + closeTwoFactorAuthWebAuthnPopout, }; diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 0175b27bd69..ad3bee97d8a 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -14,6 +14,7 @@ import { } from "@bitwarden/common/autofill/constants"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; @@ -80,6 +81,7 @@ export default class NotificationBackground { bgGetExcludedDomains: () => this.getExcludedDomains(), bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(), + notificationRefreshFlagValue: () => this.getNotificationFlag(), }; private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); @@ -137,6 +139,15 @@ export default class NotificationBackground { return await firstValueFrom(this.configService.serverConfig$); } + /** + * Gets the current value of the notification refresh feature flag + * @returns Promise indicating if the feature is enabled + */ + async getNotificationFlag(): Promise { + const flagValue = await this.configService.getFeatureFlag(FeatureFlag.NotificationRefresh); + return flagValue; + } + private async getAuthStatus() { return await firstValueFrom(this.authService.activeAccountStatus$); } diff --git a/apps/browser/src/autofill/content/components/.lit-storybook/main.ts b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts index 9e2da59d992..9068bbfc27d 100644 --- a/apps/browser/src/autofill/content/components/.lit-storybook/main.ts +++ b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts @@ -1,14 +1,14 @@ -import { dirname, join } from "path"; -import path from "path"; +import path, { dirname, join } from "path"; + import type { StorybookConfig } from "@storybook/web-components-webpack5"; -import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; import remarkGfm from "remark-gfm"; +import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; const getAbsolutePath = (value: string): string => dirname(require.resolve(join(value, "package.json"))); const config: StorybookConfig = { - stories: ["../lit-stories/**/*.lit-stories.@(js|jsx|ts|tsx)"], + stories: ["../lit-stories/**/*.lit-stories.@(js|jsx|ts|tsx)", "../lit-stories/**/*.mdx"], addons: [ getAbsolutePath("@storybook/addon-links"), getAbsolutePath("@storybook/addon-essentials"), diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/action-button.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/action-button.mdx new file mode 100644 index 00000000000..d3c1968b32f --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/action-button.mdx @@ -0,0 +1,64 @@ +import { Meta, Controls, Primary } from "@storybook/addon-docs"; + +import * as stories from "./action-button.lit-stories"; + + + +## Action Button + +The `ActionButton` component is a customizable button built using the `lit` library and styled with +`@emotion/css`. This component supports themes, handles click events, and includes a disabled state. +It is designed with accessibility and responsive design in mind. + + + + +## Props + +| **Prop** | **Type** | **Required** | **Description** | +| -------------- | -------------------------- | ------------ | ----------------------------------------------------------- | +| `buttonAction` | `(e: Event) => void` | Yes | The function to execute when the button is clicked. | +| `buttonText` | `string` | Yes | The text to display on the button. | +| `disabled` | `boolean` (default: false) | No | Disables the button when set to `true`. | +| `theme` | `Theme` | Yes | The theme to style the button. Must match the `Theme` enum. | + +## Installation and Setup + +1. Ensure you have the necessary dependencies installed: + + - `lit`: Used to render the component. + - `@emotion/css`: Used for styling the component. + +2. Pass the required props to the component when rendering: + - `buttonAction`: A function that handles the click event. + - `buttonText`: The text displayed on the button. + - `disabled` (optional): A boolean indicating whether the button is disabled. + - `theme`: The theme to style the button (must be a valid `Theme`). + +## Accessibility (WCAG) Compliance + +The `ActionButton` component follows the +[W3C ARIA button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/). Below is a breakdown of +key accessibility considerations: + +### Keyboard Accessibility + +- The button supports keyboard interaction through the `@click` event. +- Users can activate the button using the `Enter` or `Space` key. + +### Screen Reader Compatibility + +- The `title` attribute is dynamically set to the button's text (`buttonText`), ensuring it is read + by screen readers. +- The semantic `
diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts index a717786ae52..69a0fa7d9d1 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts @@ -133,6 +133,7 @@ export class PopupViewCacheService implements ViewCacheService { } private clearState() { + this._cache = {}; // clear local cache this.messageSender.send(ClEAR_VIEW_CACHE_COMMAND, {}); } } diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts index fbe94bece8c..72cfd39cd62 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts @@ -196,13 +196,15 @@ describe("popup view cache", () => { }); it("should clear on 2nd navigation", async () => { - await initServiceWithState({}); + await initServiceWithState({ temp: "state" }); await router.navigate(["a"]); expect(messageSenderMock.send).toHaveBeenCalledTimes(0); + expect(service["_cache"]).toEqual({ temp: "state" }); await router.navigate(["b"]); expect(messageSenderMock.send).toHaveBeenCalledWith(ClEAR_VIEW_CACHE_COMMAND, {}); + expect(service["_cache"]).toEqual({}); }); it("should ignore cached values when feature flag is off", async () => { diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 4d6a403a18a..1b93e33a94e 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -1,6 +1,6 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Lazy } from "@bitwarden/common/platform/misc/lazy"; diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 900212ddefa..b030ff46270 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Subject } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index 762380071b7..fe049c4f1db 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -147,7 +147,9 @@ describe("Browser Utils Service", () => { describe("isViewOpen", () => { it("returns false if a heartbeat response is not received", async () => { - BrowserApi.sendMessageWithResponse = jest.fn().mockResolvedValueOnce(undefined); + chrome.runtime.sendMessage = jest.fn().mockImplementation((message, callback) => { + callback(undefined); + }); const isViewOpen = await browserPlatformUtilsService.isViewOpen(); @@ -155,16 +157,29 @@ describe("Browser Utils Service", () => { }); it("returns true if a heartbeat response is received", async () => { - BrowserApi.sendMessageWithResponse = jest - .fn() - .mockImplementationOnce((subscriber) => - Promise.resolve((subscriber === "checkVaultPopupHeartbeat") as any), - ); + chrome.runtime.sendMessage = jest.fn().mockImplementation((message, callback) => { + callback(message.command === "checkVaultPopupHeartbeat"); + }); const isViewOpen = await browserPlatformUtilsService.isViewOpen(); expect(isViewOpen).toBe(true); }); + + it("returns false if special error is sent", async () => { + chrome.runtime.sendMessage = jest.fn().mockImplementation((message, callback) => { + chrome.runtime.lastError = new Error( + "Could not establish connection. Receiving end does not exist.", + ); + callback(undefined); + }); + + const isViewOpen = await browserPlatformUtilsService.isViewOpen(); + + expect(isViewOpen).toBe(false); + + chrome.runtime.lastError = null; + }); }); describe("copyToClipboard", () => { @@ -228,6 +243,7 @@ describe("Browser Utils Service", () => { }); it("copies the passed text using the offscreen document if the extension is using manifest v3", async () => { + BrowserApi.sendMessageWithResponse = jest.fn(); const text = "test"; offscreenDocumentService.offscreenApiSupported.mockReturnValue(true); getManifestVersionSpy.mockReturnValue(3); @@ -302,6 +318,7 @@ describe("Browser Utils Service", () => { }); it("reads the clipboard text using the offscreen document", async () => { + BrowserApi.sendMessageWithResponse = jest.fn(); offscreenDocumentService.offscreenApiSupported.mockReturnValue(true); getManifestVersionSpy.mockReturnValue(3); offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) => diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index 3679b2731e3..c9200ecc1a4 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -169,7 +169,28 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic // Query views on safari since chrome.runtime.sendMessage does not timeout and will hang. return BrowserApi.isPopupOpen(); } - return Boolean(await BrowserApi.sendMessageWithResponse("checkVaultPopupHeartbeat")); + + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ command: "checkVaultPopupHeartbeat" }, (response) => { + if (chrome.runtime.lastError != null) { + // This error means that nothing was there to listen to the message, + // meaning the view is not open. + if ( + chrome.runtime.lastError.message === + "Could not establish connection. Receiving end does not exist." + ) { + resolve(false); + return; + } + + // All unhandled errors still reject + reject(chrome.runtime.lastError); + return; + } + + resolve(Boolean(response)); + }); + }); } lockTimeout(): number { diff --git a/apps/browser/src/platform/services/popup-view-cache-background.service.ts b/apps/browser/src/platform/services/popup-view-cache-background.service.ts index f6f5e45f093..e739a3677f1 100644 --- a/apps/browser/src/platform/services/popup-view-cache-background.service.ts +++ b/apps/browser/src/platform/services/popup-view-cache-background.service.ts @@ -60,6 +60,11 @@ export class PopupViewCacheBackgroundService { ) .subscribe(); + this.messageListener + .messages$(ClEAR_VIEW_CACHE_COMMAND) + .pipe(concatMap(() => this.popupViewCacheState.update(() => null))) + .subscribe(); + merge( // on tab changed, excluding extension tabs fromChromeEvent(chrome.tabs.onActivated).pipe( diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts similarity index 55% rename from apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts rename to apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts index 0499f34a4ae..ca41127407c 100644 --- a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts +++ b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts @@ -1,11 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; -import type { BitwardenClient } from "@bitwarden/sdk-internal"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { BrowserApi } from "../../browser/browser-api"; +export type GlobalWithWasmInit = typeof globalThis & { + initSdk: () => void; +}; + // https://stackoverflow.com/a/47880734 const supported = (() => { try { @@ -17,9 +18,7 @@ const supported = (() => { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { + } catch { // ignore } return false; @@ -33,54 +32,42 @@ let loadingPromise: Promise | undefined; if (BrowserApi.isManifestVersion(3)) { if (supported) { // eslint-disable-next-line no-console - console.debug("WebAssembly is supported in this environment"); + console.info("WebAssembly is supported in this environment"); loadingPromise = import("./wasm"); } else { // eslint-disable-next-line no-console - console.debug("WebAssembly is not supported in this environment"); + console.info("WebAssembly is not supported in this environment"); loadingPromise = import("./fallback"); } } // Manifest v2 expects dynamic imports to prevent timing issues. -async function load() { +async function importModule(): Promise { if (BrowserApi.isManifestVersion(3)) { // Ensure we have loaded the module await loadingPromise; - return; - } - - if (supported) { + } else if (supported) { // eslint-disable-next-line no-console - console.debug("WebAssembly is supported in this environment"); + console.info("WebAssembly is supported in this environment"); await import("./wasm"); } else { // eslint-disable-next-line no-console - console.debug("WebAssembly is not supported in this environment"); + console.info("WebAssembly is not supported in this environment"); await import("./fallback"); } + + // the wasm and fallback imports mutate globalThis to add the initSdk function + return (globalThis as GlobalWithWasmInit).initSdk; } -/** - * SDK client factory with a js fallback for when WASM is not supported. - * - * Works both in popup and service worker. - */ -export class BrowserSdkClientFactory implements SdkClientFactory { - constructor(private logService: LogService) {} +export class BrowserSdkLoadService implements SdkLoadService { + constructor(readonly logService: LogService) {} - async createSdkClient( - ...args: ConstructorParameters - ): Promise { + async load(): Promise { const startTime = performance.now(); - await load(); - + await importModule().then((initSdk) => initSdk()); const endTime = performance.now(); - const instance = (globalThis as any).init_sdk(...args); - - this.logService.info("WASM SDK loaded in", Math.round(endTime - startTime), "ms"); - - return instance; + this.logService.info(`WASM SDK loaded in ${Math.round(endTime - startTime)}ms`); } } diff --git a/apps/browser/src/platform/services/sdk/fallback.ts b/apps/browser/src/platform/services/sdk/fallback.ts index 82d292fc9ee..cee3598feda 100644 --- a/apps/browser/src/platform/services/sdk/fallback.ts +++ b/apps/browser/src/platform/services/sdk/fallback.ts @@ -1,8 +1,8 @@ import * as sdk from "@bitwarden/sdk-internal"; import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"; -(globalThis as any).init_sdk = (...args: ConstructorParameters) => { - (sdk as any).init(wasm); +import { GlobalWithWasmInit } from "./browser-sdk-load.service"; - return new sdk.BitwardenClient(...args); +(globalThis as GlobalWithWasmInit).initSdk = () => { + (sdk as any).init(wasm); }; diff --git a/apps/browser/src/platform/services/sdk/wasm.ts b/apps/browser/src/platform/services/sdk/wasm.ts index 1977a171e23..de2eeffd294 100644 --- a/apps/browser/src/platform/services/sdk/wasm.ts +++ b/apps/browser/src/platform/services/sdk/wasm.ts @@ -1,8 +1,8 @@ import * as sdk from "@bitwarden/sdk-internal"; import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; -(globalThis as any).init_sdk = (...args: ConstructorParameters) => { - (sdk as any).init(wasm); +import { GlobalWithWasmInit } from "./browser-sdk-load.service"; - return new sdk.BitwardenClient(...args); +(globalThis as GlobalWithWasmInit).initSdk = () => { + (sdk as any).init(wasm); }; diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index b831eef0baa..70a78ce548f 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -1,21 +1,23 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; +import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; import { EnvironmentSelectorComponent, EnvironmentSelectorRouteData, ExtensionDefaultOverlayPosition, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, lockGuard, + activeAuthGuard, redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { @@ -39,8 +41,11 @@ import { DevicesIcon, SsoComponent, TwoFactorTimeoutIcon, + NewDeviceVerificationComponent, + DeviceVerificationIcon, } from "@bitwarden/auth/angular"; -import { LockComponent } from "@bitwarden/key-management/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { LockComponent } from "@bitwarden/key-management-ui"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, @@ -172,12 +177,12 @@ const routes: Routes = [ component: ExtensionAnonLayoutWrapperComponent, children: [ { - path: "2fa-timeout", + path: "authentication-timeout", canActivate: [unauthGuardFn(unauthRouteOverrides)], children: [ { path: "", - component: TwoFactorTimeoutComponent, + component: AuthenticationTimeoutComponent, }, ], data: { @@ -230,6 +235,27 @@ const routes: Routes = [ ], }, ), + { + path: "device-verification", + component: ExtensionAnonLayoutWrapperComponent, + canActivate: [ + canAccessFeature(FeatureFlag.NewDeviceVerification), + unauthGuardFn(), + activeAuthGuard(), + ], + children: [{ path: "", component: NewDeviceVerificationComponent }], + data: { + pageIcon: DeviceVerificationIcon, + pageTitle: { + key: "verifyIdentity", + }, + pageSubtitle: { + key: "weDontRecognizeThisDevice", + }, + showBackButton: true, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + }, { path: "set-password", component: SetPasswordComponent, @@ -411,7 +437,7 @@ const routes: Routes = [ data: { pageIcon: DevicesIcon, pageTitle: { - key: "loginInitiated", + key: "logInRequestSent", }, pageSubtitle: { key: "aNotificationWasSentToYourDevice", diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 24661438495..069ebf4020d 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -6,6 +6,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -22,11 +23,13 @@ export class InitService { private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, + private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { + await this.sdkLoadService.load(); await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); this.twoFactorService.init(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 24d82ab8b67..257497fb13d 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -31,8 +31,8 @@ import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarde import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { AccountService, AccountService as AccountServiceAbstraction, @@ -55,13 +55,13 @@ import { } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AnimationControlService, DefaultAnimationControlService, } from "@bitwarden/common/platform/abstractions/animation-control.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -70,6 +70,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService, @@ -82,6 +83,7 @@ import { flagEnabled } from "@bitwarden/common/platform/misc/flags"; import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; @@ -114,7 +116,7 @@ import { BiometricsService, DefaultKeyService, } from "@bitwarden/key-management"; -import { LockComponentService } from "@bitwarden/key-management/angular"; +import { LockComponentService } from "@bitwarden/key-management-ui"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; @@ -144,7 +146,7 @@ import BrowserMemoryStorageService from "../../platform/services/browser-memory- import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; -import { BrowserSdkClientFactory } from "../../platform/services/sdk/browser-sdk-client-factory"; +import { BrowserSdkLoadService } from "../../platform/services/sdk/browser-sdk-load.service"; import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; @@ -369,7 +371,7 @@ const safeProviders: SafeProvider[] = [ provide: VaultFilterService, useClass: VaultFilterService, deps: [ - OrganizationService, + DefaultOrganizationService, FolderServiceAbstraction, CipherService, CollectionService, @@ -566,11 +568,16 @@ const safeProviders: SafeProvider[] = [ deps: [MessageSender, MessageListener], }), safeProvider({ - provide: SdkClientFactory, - useFactory: (logService: LogService) => - flagEnabled("sdk") ? new BrowserSdkClientFactory(logService) : new NoopSdkClientFactory(), + provide: SdkLoadService, + useClass: BrowserSdkLoadService, deps: [LogService], }), + safeProvider({ + provide: SdkClientFactory, + useFactory: () => + flagEnabled("sdk") ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(), + deps: [], + }), safeProvider({ provide: LoginEmailService, useClass: LoginEmailService, diff --git a/apps/browser/src/services/families-policy.service.ts b/apps/browser/src/services/families-policy.service.ts deleted file mode 100644 index 426f39dcfd0..00000000000 --- a/apps/browser/src/services/families-policy.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Injectable } from "@angular/core"; -import { map, Observable, of, switchMap } from "rxjs"; - -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; - -@Injectable({ providedIn: "root" }) -export class FamiliesPolicyService { - constructor( - private policyService: PolicyService, - private organizationService: OrganizationService, - ) {} - - hasSingleEnterpriseOrg$(): Observable { - // Retrieve all organizations the user is part of - return this.organizationService.getAll$().pipe( - map((organizations) => { - // Filter to only those organizations that can manage sponsorships - const sponsorshipOrgs = organizations.filter((org) => org.canManageSponsorships); - - // Check if there is exactly one organization that can manage sponsorships. - // This is important because users that are part of multiple organizations - // may always access free bitwarden family menu. We want to restrict access - // to the policy only when there is a single enterprise organization and the free family policy is turn. - return sponsorshipOrgs.length === 1; - }), - ); - } - - isFreeFamilyPolicyEnabled$(): Observable { - return this.hasSingleEnterpriseOrg$().pipe( - switchMap((hasSingleEnterpriseOrg) => { - if (!hasSingleEnterpriseOrg) { - return of(false); - } - return this.organizationService.getAll$().pipe( - map((organizations) => organizations.find((org) => org.canManageSponsorships)?.id), - switchMap((enterpriseOrgId) => - this.policyService - .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy) - .pipe( - map( - (policies) => - policies.find((policy) => policy.organizationId === enterpriseOrgId)?.enabled ?? - false, - ), - ), - ), - ); - }), - ); - } -} diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts index 8b880e88671..a3d1c553977 100644 --- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts @@ -6,15 +6,16 @@ import { Observable, firstValueFrom, of, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { DialogService, ItemModule } from "@bitwarden/components"; +import { FamiliesPolicyService } from "../../../../billing/services/families-policy.service"; import { BrowserApi } from "../../../../platform/browser/browser-api"; import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; -import { FamiliesPolicyService } from "../../../../services/families-policy.service"; @Component({ templateUrl: "more-from-bitwarden-page-v2.component.html", @@ -43,6 +44,9 @@ export class MoreFromBitwardenPageV2Component { private familiesPolicyService: FamiliesPolicyService, private accountService: AccountService, ) { + this.familySponsorshipAvailable$ = getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => this.organizationService.familySponsorshipAvailable$(userId)), + ); this.canAccessPremium$ = this.accountService.activeAccount$.pipe( switchMap((account) => account @@ -50,7 +54,6 @@ export class MoreFromBitwardenPageV2Component { : of(false), ), ); - this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$; this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$(); this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$(); } diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index 7ff958e26ac..26aeea4f20a 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -8,38 +8,44 @@ - {{ "accountSecurity" | i18n }} + + + {{ "accountSecurity" | i18n }} - {{ "autofill" | i18n }} + + + {{ "autofill" | i18n }} - {{ "notifications" | i18n }} + + + {{ "notifications" | i18n }} - {{ "vault" | i18n }} + + + {{ "vault" | i18n }} - {{ "appearance" | i18n }} + + + {{ "appearance" | i18n }} - {{ "about" | i18n }} + + + {{ "about" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index a46f5a6955b..152c500d6ca 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -26,5 +26,15 @@ + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts index ebfb1ff765f..3252f030fc3 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts @@ -1,17 +1,19 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ActivatedRoute, Router } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, Observable } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventType } from "@bitwarden/common/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; import { CipherFormConfig, @@ -40,7 +42,7 @@ describe("AddEditV2Component", () => { const buildConfigResponse = { originalCipher: {} } as CipherFormConfig; const buildConfig = jest.fn((mode: CipherFormMode) => - Promise.resolve({ mode, ...buildConfigResponse }), + Promise.resolve({ ...buildConfigResponse, mode }), ); const queryParams$ = new BehaviorSubject({}); const disable = jest.fn(); @@ -55,9 +57,10 @@ describe("AddEditV2Component", () => { back.mockClear(); collect.mockClear(); - addEditCipherInfo$ = new BehaviorSubject(null); + addEditCipherInfo$ = new BehaviorSubject(null); cipherServiceMock = mock(); - cipherServiceMock.addEditCipherInfo$ = addEditCipherInfo$.asObservable(); + cipherServiceMock.addEditCipherInfo$ = + addEditCipherInfo$.asObservable() as Observable; await TestBed.configureTestingModule({ imports: [AddEditV2Component], @@ -71,6 +74,13 @@ describe("AddEditV2Component", () => { { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: CipherService, useValue: cipherServiceMock }, { provide: EventCollectionService, useValue: { collect } }, + { provide: LogService, useValue: mock() }, + { + provide: CipherAuthorizationService, + useValue: { + canDeleteCipher$: jest.fn().mockReturnValue(true), + }, + }, ], }) .overrideProvider(CipherFormConfigService, { @@ -92,7 +102,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("add"); + expect(buildConfig.mock.lastCall![0]).toBe("add"); expect(component.config.mode).toBe("add"); })); @@ -101,7 +111,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("clone"); + expect(buildConfig.mock.lastCall![0]).toBe("clone"); expect(component.config.mode).toBe("clone"); })); @@ -111,7 +121,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(buildConfig.mock.lastCall![0]).toBe("edit"); expect(component.config.mode).toBe("edit"); })); @@ -121,7 +131,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(buildConfig.mock.lastCall![0]).toBe("edit"); expect(component.config.mode).toBe("partial-edit"); })); }); @@ -218,7 +228,7 @@ describe("AddEditV2Component", () => { tick(); - expect(component.config.initialValues.username).toBe("identity-username"); + expect(component.config.initialValues!.username).toBe("identity-username"); })); it("overrides query params with `addEditCipherInfo` values", fakeAsync(() => { @@ -231,7 +241,7 @@ describe("AddEditV2Component", () => { tick(); - expect(component.config.initialValues.name).toBe("AddEditCipherName"); + expect(component.config.initialValues!.name).toBe("AddEditCipherName"); })); it("clears `addEditCipherInfo` after initialization", fakeAsync(() => { @@ -326,4 +336,30 @@ describe("AddEditV2Component", () => { expect(back).toHaveBeenCalled(); }); }); + + describe("delete", () => { + it("dialogService openSimpleDialog called when deleteBtn is hit", async () => { + const dialogSpy = jest + .spyOn(component["dialogService"], "openSimpleDialog") + .mockResolvedValue(true); + + await component.delete(); + expect(dialogSpy).toHaveBeenCalled(); + }); + + it("should call deleteCipher when user confirms deletion", async () => { + const deleteCipherSpy = jest.spyOn(component as any, "deleteCipher"); + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + + await component.delete(); + expect(deleteCipherSpy).toHaveBeenCalled(); + }); + + it("navigates to vault tab after deletion", async () => { + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + await component.delete(); + + expect(navigate).toHaveBeenCalledWith(["/tabs/vault"]); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 2d8c4857c1c..b46b1d61509 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -5,18 +5,27 @@ import { Component, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { firstValueFrom, map, switchMap } from "rxjs"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; -import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; +import { + AsyncActionsModule, + ButtonModule, + SearchModule, + IconButtonModule, + DialogService, + ToastService, +} from "@bitwarden/components"; import { CipherFormConfig, CipherFormConfigService, @@ -131,11 +140,13 @@ export type AddEditQueryParams = Partial>; CipherFormModule, AsyncActionsModule, PopOutComponent, + IconButtonModule, ], }) export class AddEditV2Component implements OnInit { headerText: string; config: CipherFormConfig; + canDeleteCipher$: Observable; get loading() { return this.config == null; @@ -165,6 +176,10 @@ export class AddEditV2Component implements OnInit { private router: Router, private cipherService: CipherService, private eventCollectionService: EventCollectionService, + private logService: LogService, + private toastService: ToastService, + private dialogService: DialogService, + protected cipherAuthorizationService: CipherAuthorizationService, ) { this.subscribeToParams(); } @@ -281,6 +296,10 @@ export class AddEditV2Component implements OnInit { } if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) { + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$( + config.originalCipher, + ); + await this.eventCollectionService.collect( EventType.Cipher_ClientViewed, config.originalCipher.id, @@ -337,6 +356,43 @@ export class AddEditV2Component implements OnInit { return this.i18nService.t(partOne, this.i18nService.t("typeSshKey")); } } + + delete = async () => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + await this.deleteCipher(); + } catch (e) { + this.logService.error(e); + return false; + } + + await this.router.navigate(["/tabs/vault"]); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedItem"), + }); + + return true; + }; + + protected deleteCipher() { + return this.config.originalCipher.deletedDate + ? this.cipherService.deleteWithServer(this.config.originalCipher.id) + : this.cipherService.softDeleteWithServer(this.config.originalCipher.id); + } } /** diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts index 4f6c4aa07cf..66d9096cd5c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -50,7 +50,7 @@ describe("OpenAttachmentsComponent", () => { } as Organization; const getCipher = jest.fn().mockResolvedValue(cipherDomain); - const getOrganization = jest.fn().mockResolvedValue(org); + const organizations$ = jest.fn().mockReturnValue(of([org])); const showFilePopoutMessage = jest.fn().mockReturnValue(false); const mockUserId = Utils.newGuid() as UserId; @@ -67,7 +67,7 @@ describe("OpenAttachmentsComponent", () => { openCurrentPagePopout.mockClear(); getCipher.mockClear(); showToast.mockClear(); - getOrganization.mockClear(); + organizations$.mockClear(); showFilePopoutMessage.mockClear(); hasPremiumFromAnySource$.next(true); @@ -89,7 +89,7 @@ describe("OpenAttachmentsComponent", () => { }, { provide: OrganizationService, - useValue: { get: getOrganization }, + useValue: { organizations$ }, }, { provide: FilePopoutUtilsService, @@ -148,11 +148,11 @@ describe("OpenAttachmentsComponent", () => { describe("Free Orgs", () => { beforeEach(() => { - component.cipherIsAPartOfFreeOrg = undefined; + component.cipherIsAPartOfFreeOrg = false; }); it("sets `cipherIsAPartOfFreeOrg` to false when the cipher is not a part of an organization", async () => { - cipherView.organizationId = null; + cipherView.organizationId = ""; await component.ngOnInit(); @@ -162,6 +162,7 @@ describe("OpenAttachmentsComponent", () => { it("sets `cipherIsAPartOfFreeOrg` to true when the cipher is a part of a free organization", async () => { cipherView.organizationId = "888-333-333"; org.productTierType = ProductTierType.Free; + org.id = cipherView.organizationId; await component.ngOnInit(); @@ -171,6 +172,7 @@ describe("OpenAttachmentsComponent", () => { it("sets `cipherIsAPartOfFreeOrg` to false when the organization is not free", async () => { cipherView.organizationId = "888-333-333"; org.productTierType = ProductTierType.Families; + org.id = cipherView.organizationId; await component.ngOnInit(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index 5e27ccd5c41..aca494716b1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -7,8 +7,12 @@ import { Router } from "@angular/router"; import { firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -86,7 +90,12 @@ export class OpenAttachmentsComponent implements OnInit { return; } - const org = await this.organizationService.get(cipher.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const org = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(cipher.organizationId)), + ); this.cipherIsAPartOfFreeOrg = org.productTierType === ProductTierType.Free; } diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html index eae8e2cc980..071873b40c9 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -7,4 +7,5 @@ [description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null" showAutofillButton [primaryActionAutofill]="clickItemsToAutofillVaultView" + [groupByType]="groupByType()" > diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts index d2dd21be6d8..03d84120785 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts @@ -1,5 +1,6 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; import { combineLatest, firstValueFrom, map, Observable } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -48,6 +49,10 @@ export class AutofillVaultListItemsComponent implements OnInit { clickItemsToAutofillVaultView = false; + protected groupByType = toSignal( + this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => !hasFilter)), + ); + /** * Observable that determines whether the empty autofill tip should be shown. * The tip is shown when there are no login ciphers to autofill, no filter is applied, and autofill is allowed in diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index 4c7067df53a..6e6e30b359b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -27,7 +27,7 @@ - + {{ "clone" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index 5d3dee9018e..94b4c2b855b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -9,6 +9,7 @@ import { filter } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; @@ -88,13 +89,17 @@ export class ItemMoreOptionsComponent implements OnInit { ) {} async ngOnInit(): Promise { - this.hasOrganizations = await this.organizationService.hasOrganizations(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.hasOrganizations = await firstValueFrom(this.organizationService.hasOrganizations(userId)); } get canEdit() { return this.cipher.edit; } + get canViewPassword() { + return this.cipher.viewPassword; + } /** * Determines if the cipher can be autofilled. */ diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts index d57b1d2fe36..db3fff04bbb 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -11,11 +11,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components"; +import { AddEditFolderDialogComponent } from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; -import { AddEditFolderDialogComponent } from "../add-edit-folder-dialog/add-edit-folder-dialog.component"; export interface NewItemInitialValues { folderId?: string; @@ -72,6 +72,6 @@ export class NewItemDropdownV2Component implements OnInit { } openFolderDialog() { - this.dialogService.open(AddEditFolderDialogComponent); + AddEditFolderDialogComponent.open(this.dialogService); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html index 5f958433c6d..91feaa433a9 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.html @@ -20,7 +20,7 @@

{{ numberOfAppliedFilters$ | async }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts index 3b9dc9a1647..bcea2e76190 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts @@ -6,11 +6,12 @@ import { combineLatest, map, take } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DisclosureTriggerForDirective, IconButtonModule } from "@bitwarden/components"; +import { + DisclosureComponent, + DisclosureTriggerForDirective, + IconButtonModule, +} from "@bitwarden/components"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { DisclosureComponent } from "../../../../../../../../libs/components/src/disclosure/disclosure.component"; import { runInsideAngular } from "../../../../../platform/browser/run-inside-angular.operator"; import { VaultPopupListFiltersService } from "../../../../../vault/popup/services/vault-popup-list-filters.service"; import { VaultListFiltersComponent } from "../vault-list-filters/vault-list-filters.component"; diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html index 56f35c41f6d..c61562f9f90 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html @@ -2,8 +2,9 @@
- + - + - + { + return { + organizations, + collections, + folders, + }; + }), + ); + constructor(private vaultPopupListFiltersService: VaultPopupListFiltersService) {} } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index c55e8d9fb26..2272d3fbd6c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -1,9 +1,13 @@ - + - - - - - - - - - - - - + + +

+ {{ group.subHeaderKey | i18n }} +

+
+ + + + + + + + + + + + + + + + + +
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 725aaac4666..f95790cda5f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -9,11 +9,14 @@ import { EventEmitter, inject, Input, - OnInit, Output, Signal, signal, ViewChild, + computed, + OnInit, + ChangeDetectionStrategy, + input, } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom, Observable, map } from "rxjs"; @@ -23,6 +26,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeModule, @@ -73,6 +77,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options selector: "app-vault-list-items-container", templateUrl: "vault-list-items-container.component.html", standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, }) export class VaultListItemsContainerComponent implements OnInit, AfterViewInit { private compactModeService = inject(CompactModeService); @@ -110,11 +115,51 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit { */ private viewCipherTimeout: number | null; + ciphers = input([]); + /** - * The list of ciphers to display. + * If true, we will group ciphers by type (Login, Card, Identity) + * within subheadings in a single container, converted to a WritableSignal. */ - @Input() - ciphers: PopupCipherView[] = []; + groupByType = input(false); + + /** + * Computed signal for a grouped list of ciphers with an optional header + */ + cipherGroups$ = computed< + { + subHeaderKey?: string | null; + ciphers: PopupCipherView[]; + }[] + >(() => { + const groups: { [key: string]: CipherView[] } = {}; + + this.ciphers().forEach((cipher) => { + let groupKey; + + if (this.groupByType()) { + switch (cipher.type) { + case CipherType.Card: + groupKey = "cards"; + break; + case CipherType.Identity: + groupKey = "identities"; + break; + } + } + + if (!groups[groupKey]) { + groups[groupKey] = []; + } + + groups[groupKey].push(cipher); + }); + + return Object.keys(groups).map((key) => ({ + subHeaderKey: this.groupByType ? key : "", + ciphers: groups[key], + })); + }); /** * Title for the vault list item section. diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts index 67e069d388a..e20bb1f1bcd 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts @@ -11,10 +11,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { PasswordHistoryViewComponent } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.component"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 43471e56e7b..6e017042711 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -69,13 +69,13 @@ { + this.vaultScrollPositionService.start(this.virtualScrollElement!); + }); + } + } + async ngOnInit() { this.cipherService.failedToDecryptCiphers$ .pipe( @@ -134,5 +150,7 @@ export class VaultV2Component implements OnInit, OnDestroy { }); } - ngOnDestroy(): void {} + ngOnDestroy(): void { + this.vaultScrollPositionService.stop(); + } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index 526ab2e2579..39feb86f4fd 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -21,12 +21,15 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { DialogService, ToastService } from "@bitwarden/components"; import { CopyCipherFieldService } from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; +import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service"; import { ViewV2Component } from "./view-v2.component"; @@ -44,6 +47,10 @@ describe("ViewV2Component", () => { const collect = jest.fn().mockResolvedValue(null); const doAutofill = jest.fn().mockResolvedValue(true); const copy = jest.fn().mockResolvedValue(true); + const back = jest.fn().mockResolvedValue(null); + const openSimpleDialog = jest.fn().mockResolvedValue(true); + const stop = jest.fn(); + const showToast = jest.fn(); const mockCipher = { id: "122-333-444", @@ -54,7 +61,7 @@ describe("ViewV2Component", () => { password: "test-password", totp: "123", }, - }; + } as unknown as CipherView; const mockVaultPopupAutofillService = { doAutofill, @@ -68,13 +75,21 @@ describe("ViewV2Component", () => { const mockCipherService = { get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }), getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}), + deleteWithServer: jest.fn().mockResolvedValue(undefined), + softDeleteWithServer: jest.fn().mockResolvedValue(undefined), }; beforeEach(async () => { + mockCipherService.deleteWithServer.mockClear(); + mockCipherService.softDeleteWithServer.mockClear(); mockNavigate.mockClear(); collect.mockClear(); doAutofill.mockClear(); copy.mockClear(); + stop.mockClear(); + openSimpleDialog.mockClear(); + back.mockClear(); + showToast.mockClear(); await TestBed.configureTestingModule({ imports: [ViewV2Component], @@ -84,9 +99,12 @@ describe("ViewV2Component", () => { { provide: LogService, useValue: mock() }, { provide: PlatformUtilsService, useValue: mock() }, { provide: ConfigService, useValue: mock() }, - { provide: PopupRouterCacheService, useValue: mock() }, + { provide: PopupRouterCacheService, useValue: mock({ back }) }, { provide: ActivatedRoute, useValue: { queryParams: params$ } }, { provide: EventCollectionService, useValue: { collect } }, + { provide: VaultPopupScrollPositionService, useValue: { stop } }, + { provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService }, + { provide: ToastService, useValue: { showToast } }, { provide: I18nService, useValue: { @@ -98,7 +116,6 @@ describe("ViewV2Component", () => { }, }, }, - { provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService }, { provide: AccountService, useValue: accountService, @@ -114,7 +131,13 @@ describe("ViewV2Component", () => { useValue: mockCopyCipherFieldService, }, ], - }).compileComponents(); + }) + .overrideProvider(DialogService, { + useValue: { + openSimpleDialog, + }, + }) + .compileComponents(); fixture = TestBed.createComponent(ViewV2Component); component = fixture.componentInstance; @@ -223,4 +246,130 @@ describe("ViewV2Component", () => { expect(closeSpy).toHaveBeenCalledTimes(1); })); }); + + describe("delete", () => { + beforeEach(() => { + component.cipher = mockCipher; + }); + + it("opens confirmation modal", async () => { + await component.delete(); + + expect(openSimpleDialog).toHaveBeenCalledTimes(1); + }); + + it("navigates back", async () => { + await component.delete(); + + expect(back).toHaveBeenCalledTimes(1); + }); + + it("stops scroll position service", async () => { + await component.delete(); + + expect(stop).toHaveBeenCalledTimes(1); + expect(stop).toHaveBeenCalledWith(true); + }); + + describe("deny confirmation", () => { + beforeEach(() => { + openSimpleDialog.mockResolvedValue(false); + }); + + it("does not delete the cipher", async () => { + await component.delete(); + + expect(mockCipherService.deleteWithServer).not.toHaveBeenCalled(); + expect(mockCipherService.softDeleteWithServer).not.toHaveBeenCalled(); + }); + + it("does not interact with side effects", () => { + expect(back).not.toHaveBeenCalled(); + expect(stop).not.toHaveBeenCalled(); + expect(showToast).not.toHaveBeenCalled(); + }); + }); + + describe("accept confirmation", () => { + beforeEach(() => { + openSimpleDialog.mockResolvedValue(true); + }); + + describe("soft delete", () => { + beforeEach(() => { + (mockCipher as any).isDeleted = null; + }); + + it("opens confirmation dialog", async () => { + await component.delete(); + + expect(openSimpleDialog).toHaveBeenCalledTimes(1); + expect(openSimpleDialog).toHaveBeenCalledWith({ + content: { + key: "deleteItemConfirmation", + }, + title: { + key: "deleteItem", + }, + type: "warning", + }); + }); + + it("calls soft delete", async () => { + await component.delete(); + + expect(mockCipherService.softDeleteWithServer).toHaveBeenCalled(); + expect(mockCipherService.deleteWithServer).not.toHaveBeenCalled(); + }); + + it("shows toast", async () => { + await component.delete(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: "deletedItem", + }); + }); + }); + + describe("hard delete", () => { + beforeEach(() => { + (mockCipher as any).isDeleted = true; + }); + + it("opens confirmation dialog", async () => { + await component.delete(); + + expect(openSimpleDialog).toHaveBeenCalledTimes(1); + expect(openSimpleDialog).toHaveBeenCalledWith({ + content: { + key: "permanentlyDeleteItemConfirmation", + }, + title: { + key: "deleteItem", + }, + type: "warning", + }); + }); + + it("calls soft delete", async () => { + await component.delete(); + + expect(mockCipherService.deleteWithServer).toHaveBeenCalled(); + expect(mockCipherService.softDeleteWithServer).not.toHaveBeenCalled(); + }); + + it("shows toast", async () => { + await component.delete(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: "permanentlyDeletedItem", + }); + }); + }); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index f3cd713dd5f..65fb024ee99 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -23,6 +23,7 @@ import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -35,20 +36,15 @@ import { SearchModule, ToastService, } from "@bitwarden/components"; -import { CopyCipherFieldService } from "@bitwarden/vault"; +import { CipherViewComponent, CopyCipherFieldService } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { PremiumUpgradePromptService } from "../../../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service"; import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.service"; +import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; import { closeViewVaultItemPopout, VaultPopoutType } from "../../../utils/vault-popout-window"; import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component"; @@ -113,6 +109,7 @@ export class ViewV2Component { private popupRouterCacheService: PopupRouterCacheService, protected cipherAuthorizationService: CipherAuthorizationService, private copyCipherFieldService: CopyCipherFieldService, + private popupScrollPositionService: VaultPopupScrollPositionService, ) { this.subscribeToParams(); } @@ -202,6 +199,7 @@ export class ViewV2Component { return false; } + this.popupScrollPositionService.stop(true); await this.popupRouterCacheService.back(); this.toastService.showToast({ diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts index 2dad1e3034c..75352c1331a 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts @@ -21,14 +21,12 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { AutoFillOptions, AutofillService, PageDetail, } from "../../../autofill/services/abstractions/autofill.service"; +import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts index ff282d7a6d0..82188ef823b 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts @@ -27,13 +27,11 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view import { ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { AutofillService, PageDetail, } from "../../../autofill/services/abstractions/autofill.service"; +import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { closeViewVaultItemPopout, VaultPopoutType } from "../utils/vault-popout-window"; diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 528aae111cc..ec20458ca60 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -6,18 +6,18 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { ObservableTracker } from "@bitwarden/common/spec"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { ObservableTracker, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; +import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; import { VaultPopupAutofillService } from "./vault-popup-autofill.service"; @@ -43,6 +43,8 @@ describe("VaultPopupItemsService", () => { const vaultAutofillServiceMock = mock(); const syncServiceMock = mock(); const inlineMenuFieldQualificationServiceMock = mock(); + const userId = Utils.newGuid() as UserId; + const accountServiceMock = mockAccountServiceWith(userId); beforeEach(() => { allCiphers = cipherFactory(10); @@ -99,7 +101,7 @@ describe("VaultPopupItemsService", () => { { id: "col2", name: "Collection 2" } as CollectionView, ]; - organizationServiceMock.organizations$ = new BehaviorSubject([mockOrg]); + organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg])); collectionService.decryptedCollections$ = new BehaviorSubject(mockCollections); activeUserLastSync$ = new BehaviorSubject(new Date()); @@ -111,6 +113,7 @@ describe("VaultPopupItemsService", () => { { provide: VaultSettingsService, useValue: vaultSettingsServiceMock }, { provide: SearchService, useValue: searchService }, { provide: OrganizationService, useValue: organizationServiceMock }, + { provide: AccountService, useValue: accountServiceMock }, { provide: VaultPopupListFiltersService, useValue: vaultPopupListFiltersServiceMock }, { provide: CollectionService, useValue: collectionService }, { provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock }, diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index fb230df7953..8e0711abb1e 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -24,6 +24,7 @@ import { import { CollectionService } from "@bitwarden/admin-console/common"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; @@ -56,6 +57,9 @@ export class VaultPopupItemsService { latestSearchText$: Observable = this._searchText$.asObservable(); + private organizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + ); /** * Observable that contains the list of other cipher types that should be shown * in the autofill section of the Vault tab. Depends on vault settings. @@ -67,9 +71,9 @@ export class VaultPopupItemsService { this.vaultPopupAutofillService.nonLoginCipherTypesOnPage$, ]).pipe( map(([showCardsSettingEnabled, showIdentitiesSettingEnabled, nonLoginCipherTypesOnPage]) => { - const showCards = showCardsSettingEnabled && nonLoginCipherTypesOnPage[CipherType.Card]; + const showCards = showCardsSettingEnabled || nonLoginCipherTypesOnPage[CipherType.Card]; const showIdentities = - showIdentitiesSettingEnabled && nonLoginCipherTypesOnPage[CipherType.Identity]; + showIdentitiesSettingEnabled || nonLoginCipherTypesOnPage[CipherType.Identity]; return [ ...(showCards ? [CipherType.Card] : []), @@ -97,10 +101,7 @@ export class VaultPopupItemsService { private _activeCipherList$: Observable = this._allDecryptedCiphers$.pipe( switchMap((ciphers) => - combineLatest([ - this.organizationService.organizations$, - this.collectionService.decryptedCollections$, - ]).pipe( + combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe( map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); @@ -213,6 +214,7 @@ export class VaultPopupItemsService { map(([hasSearchText, filters]) => { return hasSearchText || Object.values(filters).some((filter) => filter !== null); }), + shareReplay({ bufferSize: 1, refCount: true }), ); /** @@ -232,7 +234,7 @@ export class VaultPopupItemsService { /** Observable that indicates when the user should see the deactivated org state */ showDeactivatedOrg$: Observable = combineLatest([ this.vaultPopupListFiltersService.filters$.pipe(distinctUntilKeyChanged("organization")), - this.organizationService.organizations$, + this.organizations$, ]).pipe( map(([filters, orgs]) => { if (!filters.organization || filters.organization.id === MY_VAULT_ID) { @@ -249,10 +251,7 @@ export class VaultPopupItemsService { */ deletedCiphers$: Observable = this._allDecryptedCiphers$.pipe( switchMap((ciphers) => - combineLatest([ - this.organizationService.organizations$, - this.collectionService.decryptedCollections$, - ]).pipe( + combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe( map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); @@ -281,6 +280,7 @@ export class VaultPopupItemsService { private collectionService: CollectionService, private vaultPopupAutofillService: VaultPopupAutofillService, private syncService: SyncService, + private accountService: AccountService, ) {} applyFilter(newSearchText: string) { diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index e1236be08f9..7f570e8f5c9 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -23,7 +23,9 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi describe("VaultPopupListFiltersService", () => { let service: VaultPopupListFiltersService; - const memberOrganizations$ = new BehaviorSubject([]); + const _memberOrganizations$ = new BehaviorSubject([]); + const memberOrganizations$ = (userId: UserId) => _memberOrganizations$; + const organizations$ = new BehaviorSubject([]); const folderViews$ = new BehaviorSubject([]); const cipherViews$ = new BehaviorSubject({}); const decryptedCollections$ = new BehaviorSubject([]); @@ -44,6 +46,7 @@ describe("VaultPopupListFiltersService", () => { const organizationService = { memberOrganizations$, + organizations$, } as unknown as OrganizationService; const i18nService = { @@ -58,7 +61,7 @@ describe("VaultPopupListFiltersService", () => { const update = jest.fn().mockResolvedValue(undefined); beforeEach(() => { - memberOrganizations$.next([]); + _memberOrganizations$.next([]); decryptedCollections$.next([]); policyAppliesToActiveUser$.next(false); policyService.policyAppliesToActiveUser$.mockClear(); @@ -135,7 +138,7 @@ describe("VaultPopupListFiltersService", () => { describe("organizations$", () => { it('does not add "myVault" to the list of organizations when there are no organizations', (done) => { - memberOrganizations$.next([]); + _memberOrganizations$.next([]); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([]); @@ -145,7 +148,7 @@ describe("VaultPopupListFiltersService", () => { it('adds "myVault" to the list of organizations when there are other organizations', (done) => { const orgs = [{ name: "bobby's org", id: "1234-3323-23223" }] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual(["myVault", "bobby's org"]); @@ -158,7 +161,7 @@ describe("VaultPopupListFiltersService", () => { { name: "bobby's org", id: "1234-3323-23223" }, { name: "alice's org", id: "2223-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -179,7 +182,7 @@ describe("VaultPopupListFiltersService", () => { it("returns an empty array when the policy applies and there is a single organization", (done) => { policyAppliesToActiveUser$.next(true); - memberOrganizations$.next([ + _memberOrganizations$.next([ { name: "bobby's org", id: "1234-3323-23223" }, ] as Organization[]); @@ -196,7 +199,7 @@ describe("VaultPopupListFiltersService", () => { { name: "alice's org", id: "2223-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -216,7 +219,7 @@ describe("VaultPopupListFiltersService", () => { { name: "catherine's org", id: "77733-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -240,7 +243,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual(["bwi-user", "bwi-family"]); @@ -258,7 +261,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual(["bwi-user", "bwi-family"]); @@ -276,7 +279,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual([ diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 8455fd587d0..579319c92ab 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -208,7 +208,9 @@ export class VaultPopupListFiltersService { * Organization array structured to be directly passed to `ChipSelectComponent` */ organizations$: Observable[]> = combineLatest([ - this.organizationService.memberOrganizations$, + this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.memberOrganizations$(account?.id)), + ), this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), ]).pipe( map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [ @@ -368,6 +370,9 @@ export class VaultPopupListFiltersService { ), ); + /** Organizations, collection, folders filters. */ + allFilters$ = combineLatest([this.organizations$, this.collections$, this.folders$]); + /** Updates the stored state for filter visibility. */ async updateFilterVisibility(isVisible: boolean): Promise { await this.filterVisibilityState.update(() => isVisible); diff --git a/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.spec.ts new file mode 100644 index 00000000000..562375f8f85 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.spec.ts @@ -0,0 +1,137 @@ +import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling"; +import { fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { NavigationEnd, Router } from "@angular/router"; +import { Subject, Subscription } from "rxjs"; + +import { VaultPopupScrollPositionService } from "./vault-popup-scroll-position.service"; + +describe("VaultPopupScrollPositionService", () => { + let service: VaultPopupScrollPositionService; + const events$ = new Subject(); + const unsubscribe = jest.fn(); + + beforeEach(async () => { + unsubscribe.mockClear(); + + await TestBed.configureTestingModule({ + providers: [ + VaultPopupScrollPositionService, + { provide: Router, useValue: { events: events$ } }, + ], + }); + + service = TestBed.inject(VaultPopupScrollPositionService); + + // set up dummy values + service["scrollPosition"] = 234; + service["scrollSubscription"] = { unsubscribe } as unknown as Subscription; + }); + + describe("router events", () => { + it("does not reset service when navigating to `/tabs/vault`", fakeAsync(() => { + const event = new NavigationEnd(22, "/tabs/vault", ""); + events$.next(event); + + tick(); + + expect(service["scrollPosition"]).toBe(234); + expect(service["scrollSubscription"]).not.toBeNull(); + })); + + it("resets values when navigating to other tab pages", fakeAsync(() => { + const event = new NavigationEnd(23, "/tabs/generator", ""); + events$.next(event); + + tick(); + + expect(service["scrollPosition"]).toBeNull(); + expect(unsubscribe).toHaveBeenCalled(); + expect(service["scrollSubscription"]).toBeNull(); + })); + }); + + describe("stop", () => { + it("removes scroll listener", () => { + service.stop(); + + expect(unsubscribe).toHaveBeenCalledTimes(1); + expect(service["scrollSubscription"]).toBeNull(); + }); + + it("resets stored values", () => { + service.stop(true); + + expect(service["scrollPosition"]).toBeNull(); + }); + }); + + describe("start", () => { + const elementScrolled$ = new Subject(); + const focus = jest.fn(); + const nativeElement = { + scrollTop: 0, + querySelector: jest.fn(() => ({ focus })), + addEventListener: jest.fn(), + style: { + visibility: "", + }, + }; + const virtualElement = { + elementScrolled: () => elementScrolled$, + getElementRef: () => ({ nativeElement }), + scrollTo: jest.fn(), + } as unknown as CdkVirtualScrollableElement; + + afterEach(() => { + // remove the actual subscription created by `.subscribe` + service["scrollSubscription"]?.unsubscribe(); + }); + + describe("initial scroll position", () => { + beforeEach(() => { + (virtualElement.scrollTo as jest.Mock).mockClear(); + nativeElement.querySelector.mockClear(); + }); + + it("does not scroll when `scrollPosition` is null", () => { + service["scrollPosition"] = null; + + service.start(virtualElement); + + expect(virtualElement.scrollTo).not.toHaveBeenCalled(); + }); + + it("scrolls the virtual element to `scrollPosition`", fakeAsync(() => { + service["scrollPosition"] = 500; + nativeElement.scrollTop = 500; + + service.start(virtualElement); + tick(); + + expect(virtualElement.scrollTo).toHaveBeenCalledWith({ behavior: "instant", top: 500 }); + })); + }); + + describe("scroll listener", () => { + it("unsubscribes from any existing subscription", () => { + service.start(virtualElement); + + expect(unsubscribe).toHaveBeenCalled(); + }); + + it("subscribes to `elementScrolled`", fakeAsync(() => { + virtualElement.measureScrollOffset = jest.fn(() => 455); + + service.start(virtualElement); + + elementScrolled$.next(null); // first subscription is skipped by `skip(1)` + elementScrolled$.next(null); + tick(); + + expect(virtualElement.measureScrollOffset).toHaveBeenCalledTimes(1); + expect(virtualElement.measureScrollOffset).toHaveBeenCalledWith("top"); + expect(service["scrollPosition"]).toBe(455); + })); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.ts b/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.ts new file mode 100644 index 00000000000..5bfe0ec9331 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-scroll-position.service.ts @@ -0,0 +1,81 @@ +import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling"; +import { inject, Injectable } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { NavigationEnd, Router } from "@angular/router"; +import { filter, skip, Subscription } from "rxjs"; + +@Injectable({ + providedIn: "root", +}) +export class VaultPopupScrollPositionService { + private router = inject(Router); + + /** Path of the vault screen */ + private readonly vaultPath = "/tabs/vault"; + + /** Current scroll position relative to the top of the viewport. */ + private scrollPosition: number | null = null; + + /** Subscription associated with the virtual scroll element. */ + private scrollSubscription: Subscription | null = null; + + constructor() { + this.router.events + .pipe( + takeUntilDestroyed(), + filter((event): event is NavigationEnd => event instanceof NavigationEnd), + ) + .subscribe((event) => { + this.resetListenerForNavigation(event); + }); + } + + /** Scrolls the user to the stored scroll position and starts tracking scroll of the page. */ + start(virtualScrollElement: CdkVirtualScrollableElement) { + if (this.hasScrollPosition()) { + // Use `setTimeout` to scroll after rendering is complete + setTimeout(() => { + virtualScrollElement.scrollTo({ top: this.scrollPosition!, behavior: "instant" }); + }); + } + + this.scrollSubscription?.unsubscribe(); + + // Skip the first scroll event to avoid settings the scroll from the above `scrollTo` call + this.scrollSubscription = virtualScrollElement + ?.elementScrolled() + .pipe(skip(1)) + .subscribe(() => { + const offset = virtualScrollElement.measureScrollOffset("top"); + this.scrollPosition = offset; + }); + } + + /** Stops the scroll listener from updating the stored location. */ + stop(reset?: true) { + this.scrollSubscription?.unsubscribe(); + this.scrollSubscription = null; + + if (reset) { + this.scrollPosition = null; + } + } + + /** Returns true when a scroll position has been stored. */ + hasScrollPosition() { + return this.scrollPosition !== null; + } + + /** Conditionally resets the scroll listeners based on the ending path of the navigation */ + private resetListenerForNavigation(event: NavigationEnd): void { + // The vault page is the target of the scroll listener, return early + if (event.url === this.vaultPath) { + return; + } + + // For all other tab pages reset the scroll position + if (event.url.startsWith("/tabs/")) { + this.stop(true); + } + } +} diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts index 3aab9f935e4..deddbd444fc 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -14,17 +14,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { BadgeModule, CheckboxModule, Option } from "@bitwarden/components"; +import { + BadgeModule, + CardComponent, + CheckboxModule, + FormFieldModule, + Option, + SelectModule, +} from "@bitwarden/components"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { CardComponent } from "../../../../../../libs/components/src/card/card.component"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { SelectModule } from "../../../../../../libs/components/src/select/select.module"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts index 9c202e26fef..6689f5a6c6d 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts @@ -14,10 +14,10 @@ import { UserId } from "@bitwarden/common/types/guid"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { DialogService } from "@bitwarden/components"; +import { AddEditFolderDialogComponent } from "@bitwarden/vault"; import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; -import { AddEditFolderDialogComponent } from "../components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component"; import { FoldersV2Component } from "./folders-v2.component"; @@ -27,8 +27,8 @@ import { FoldersV2Component } from "./folders-v2.component"; template: ``, }) class MockPopupHeaderComponent { - @Input() pageTitle: string; - @Input() backAction: () => void; + @Input() pageTitle: string = ""; + @Input() backAction: () => void = () => {}; } @Component({ @@ -37,14 +37,15 @@ class MockPopupHeaderComponent { template: ``, }) class MockPopupFooterComponent { - @Input() pageTitle: string; + @Input() pageTitle: string = ""; } describe("FoldersV2Component", () => { let component: FoldersV2Component; let fixture: ComponentFixture; const folderViews$ = new BehaviorSubject([]); - const open = jest.fn(); + const open = jest.spyOn(AddEditFolderDialogComponent, "open"); + const mockDialogService = { open: jest.fn() }; beforeEach(async () => { open.mockClear(); @@ -68,7 +69,7 @@ describe("FoldersV2Component", () => { imports: [MockPopupHeaderComponent, MockPopupFooterComponent], }, }) - .overrideProvider(DialogService, { useValue: { open } }) + .overrideProvider(DialogService, { useValue: mockDialogService }) .compileComponents(); fixture = TestBed.createComponent(FoldersV2Component); @@ -101,9 +102,7 @@ describe("FoldersV2Component", () => { editButton.triggerEventHandler("click"); - expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { - data: { editFolderConfig: { folder } }, - }); + expect(open).toHaveBeenCalledWith(mockDialogService, { editFolderConfig: { folder } }); }); it("opens add dialog for new folder when there are no folders", () => { @@ -114,6 +113,6 @@ describe("FoldersV2Component", () => { addButton.triggerEventHandler("click"); - expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { data: {} }); + expect(open).toHaveBeenCalledWith(mockDialogService, {}); }); }); diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.ts index 8abc3f906c0..f71374e5305 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.ts @@ -12,25 +12,14 @@ import { ButtonModule, DialogService, IconButtonModule, + ItemModule, + NoItemsModule, } from "@bitwarden/components"; -import { VaultIcons } from "@bitwarden/vault"; +import { AddEditFolderDialogComponent, VaultIcons } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ItemGroupComponent } from "../../../../../../libs/components/src/item/item-group.component"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ItemModule } from "../../../../../../libs/components/src/item/item.module"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { NoItemsModule } from "../../../../../../libs/components/src/no-items/no-items.module"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; -import { - AddEditFolderDialogComponent, - AddEditFolderDialogData, -} from "../components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component"; @Component({ standalone: true, @@ -42,7 +31,6 @@ import { PopupPageComponent, PopupHeaderComponent, ItemModule, - ItemGroupComponent, NoItemsModule, IconButtonModule, ButtonModule, @@ -78,8 +66,6 @@ export class FoldersV2Component { // If a folder is provided, the edit variant should be shown const editFolderConfig = folder ? { folder } : undefined; - this.dialogService.open(AddEditFolderDialogComponent, { - data: { editFolderConfig }, - }); + AddEditFolderDialogComponent.open(this.dialogService, { editFolderConfig }); } } diff --git a/apps/browser/src/vault/services/vault-filter.service.ts b/apps/browser/src/vault/services/vault-filter.service.ts index 305c7de487b..f8b22f2f88f 100644 --- a/apps/browser/src/vault/services/vault-filter.service.ts +++ b/apps/browser/src/vault/services/vault-filter.service.ts @@ -38,7 +38,7 @@ export class VaultFilterService extends BaseVaultFilterService { this.vaultFilter.myVaultOnly = false; this.vaultFilter.selectedOrganizationId = null; - this.accountService.activeAccount$.subscribe((account) => { + accountService.activeAccount$.subscribe((account) => { this.setVaultFilter(this.allVaults); }); } diff --git a/apps/browser/store/locales/nb/copy.resx b/apps/browser/store/locales/nb/copy.resx index b496e223cbe..29d612906c7 100644 --- a/apps/browser/store/locales/nb/copy.resx +++ b/apps/browser/store/locales/nb/copy.resx @@ -118,58 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden passordbehandler + Bitwarden Passordbehandler - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Hjemme, på jobben eller på farten sikrer Bitwarden enkelt alle passordene dine, passordene og sensitiv informasjon. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Anerkjent som den beste passordbehandleren av PCMag, WIRED, The Verge, CNET, G2 og mer! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +SIKRE DIT DIGITALE LIV +Sikre ditt digitale liv og beskytte mot datainnbrudd ved å generere og lagre unike, sterke passord for hver konto. Oppretthold alt i et ende-til-ende kryptert passordhvelv som bare du har tilgang til. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +FÅ TILGANG TIL DINE DATA, HVOR SOM HELST, NÅR som helst, PÅ ENHVER ENHET +Administrer, lagre, sikre og del ubegrensede passord på tvers av ubegrensede enheter uten begrensninger. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +ALLE BØR HA VERKTØYET FOR Å HOLDE SIKKERHET PÅ PÅ NETT +Bruk Bitwarden gratis uten annonser eller salgsdata. Bitwarden mener alle bør ha muligheten til å være trygge på nettet. Premium-planer gir tilgang til avanserte funksjoner. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +STYR LAGEN DINE MED BITWARDEN +Planer for Teams og Enterprise kommer med profesjonelle forretningsfunksjoner. Noen eksempler inkluderer SSO-integrasjon, selvhosting, katalogintegrering og SCIM-klargjøring, globale retningslinjer, API-tilgang, hendelseslogger og mer. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Bruk Bitwarden til å sikre arbeidsstyrken din og dele sensitiv informasjon med kolleger. -More reasons to choose Bitwarden: +Flere grunner til å velge Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Kryptering i verdensklasse +Passord er beskyttet med avansert ende-til-ende-kryptering (AES-256 bit, saltet hashtag og PBKDF2 SHA-256) slik at dataene dine forblir sikre og private. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Tredjepartsrevisjoner +Bitwarden gjennomfører regelmessig omfattende tredjeparts sikkerhetsrevisjoner med bemerkelsesverdige sikkerhetsfirmaer. Disse årlige revisjonene inkluderer kildekodevurderinger og penetrasjonstesting på tvers av Bitwarden IP-er, servere og webapplikasjoner. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Avansert 2FA +Sikre påloggingen din med en tredjeparts autentisering, e-postkoder eller FIDO2 WebAuthn-legitimasjon som en maskinvaresikkerhetsnøkkel eller passord. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Overfør data direkte til andre mens du opprettholder ende-til-ende kryptert sikkerhet og begrenser eksponeringen. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Innebygd generator +Lag lange, komplekse og distinkte passord og unike brukernavn for hvert nettsted du besøker. Integrer med e-postaliasleverandører for ekstra personvern. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Globale oversettelser +Bitwarden-oversettelser finnes for mer enn 60 språk, oversatt av det globale samfunnet gjennom Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Applikasjoner på tvers av plattformer +Sikre og del sensitive data i Bitwarden Vault fra hvilken som helst nettleser, mobilenhet eller stasjonær OS, og mer. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden sikrer mer enn bare passord +End-to-end krypterte legitimasjonsadministrasjonsløsninger fra Bitwarden gir organisasjoner mulighet til å sikre alt, inkludert utviklerhemmeligheter og passordopplevelser. Besøk Bitwarden.com for å lære mer om Bitwarden Secrets Manager og Bitwarden Passwordless.dev! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Hjemme, på jobben eller på farten sikrer Bitwarden enkelt alle passordene dine, passordene og sensitiv informasjon. Synkroniser og få tilgang til ditt hvelv fra alle dine enheter diff --git a/apps/browser/tailwind.config.js b/apps/browser/tailwind.config.js index d0ec8025c66..f0a7db8e8bc 100644 --- a/apps/browser/tailwind.config.js +++ b/apps/browser/tailwind.config.js @@ -1,11 +1,11 @@ -/* eslint-disable no-undef, @typescript-eslint/no-var-requires */ +/* eslint-disable no-undef, @typescript-eslint/no-require-imports */ const config = require("../../libs/components/tailwind.config.base"); config.content = [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", - "../../libs/key-management/src/**/*.{html,ts}", + "../../libs/key-management-ui/src/**/*.{html,ts}", "../../libs/vault/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}", "../../libs/vault/src/**/*.{html,ts}", diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 2326e68f55d..81b9bc870c4 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -24,10 +24,10 @@ "@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"], - "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/importer-core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/key-management": ["../../libs/key-management/src"], - "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], + "@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["../../libs/tools/card/src"], @@ -51,8 +51,8 @@ }, "include": [ "src", - "../../libs/common/src/platform/services/**/*.worker.ts", "../../libs/common/src/autofill/constants", - "../../libs/common/custom-matchers.d.ts" + "../../libs/common/custom-matchers.d.ts", + "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts" ] } diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index bce41d64d1f..ba60a577a71 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -205,7 +205,7 @@ const mainConfig = { "./src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts", "overlay/list": "./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts", - "encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts", + "encrypt-worker": "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts", "content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts", }, optimization: { diff --git a/apps/cli/.eslintrc.json b/apps/cli/.eslintrc.json deleted file mode 100644 index 10d22388378..00000000000 --- a/apps/cli/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "node": true - } -} diff --git a/apps/cli/package.json b/apps/cli/package.json index d1d8ac76ec4..391c9c80808 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2025.1.1", + "version": "2025.1.3", "keywords": [ "bitwarden", "password", @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.71", + "tldts": "6.1.74", "zxcvbn": "4.4.2" } } diff --git a/apps/cli/src/admin-console/.eslintrc.json b/apps/cli/src/admin-console/.eslintrc.json deleted file mode 100644 index 38467187294..00000000000 --- a/apps/cli/src/admin-console/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../../libs/admin-console/.eslintrc.json" -} diff --git a/apps/cli/src/admin-console/commands/confirm.command.ts b/apps/cli/src/admin-console/commands/confirm.command.ts index 5f5f58163e3..0d5c7ba069c 100644 --- a/apps/cli/src/admin-console/commands/confirm.command.ts +++ b/apps/cli/src/admin-console/commands/confirm.command.ts @@ -5,7 +5,7 @@ import { OrganizationUserConfirmRequest, } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { KeyService } from "@bitwarden/key-management"; diff --git a/apps/cli/src/commands/download.command.ts b/apps/cli/src/commands/download.command.ts index 6a7cda2ac91..01ef675d2a8 100644 --- a/apps/cli/src/commands/download.command.ts +++ b/apps/cli/src/commands/download.command.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 13152f3d29e..9af28863c09 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -6,10 +6,10 @@ import { CollectionRequest } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; import { FolderExport } from "@bitwarden/common/models/export/folder.export"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; @@ -123,6 +123,9 @@ export class EditCommand { "Item does not belong to an organization. Consider moving it first.", ); } + if (!cipher.viewPassword) { + return Response.noEditPermission(); + } cipher.collectionIds = req; try { diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index a1fec7a7472..a90bfa64894 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -10,8 +10,10 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CardExport } from "@bitwarden/common/models/export/card.export"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; @@ -22,7 +24,6 @@ import { LoginUriExport } from "@bitwarden/common/models/export/login-uri.export import { LoginExport } from "@bitwarden/common/models/export/login.export"; import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; @@ -479,10 +480,18 @@ export class GetCommand extends DownloadCommand { private async getOrganization(id: string) { let org: Organization = null; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (!userId) { + return Response.badRequest("No user found."); + } if (Utils.isGuid(id)) { - org = await this.organizationService.getFromState(id); + org = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === id))), + ); } else if (id.trim() !== "") { - let orgs = await firstValueFrom(this.organizationService.organizations$); + let orgs = await firstValueFrom(this.organizationService.organizations$(userId)); orgs = CliUtils.searchOrganizations(orgs, id); if (orgs.length > 1) { return Response.multipleResults(orgs.map((c) => c.id)); diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 92da86b696a..5e01af798a4 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -13,6 +13,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -177,7 +178,15 @@ export class ListCommand { if (!Utils.isGuid(options.organizationId)) { return Response.badRequest("`" + options.organizationId + "` is not a GUID."); } - const organization = await this.organizationService.getFromState(options.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (!userId) { + return Response.badRequest("No user found."); + } + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizatons) => organizatons.find((o) => o.id == options.organizationId))), + ); if (organization == null) { return Response.error("Organization not found."); } @@ -210,7 +219,16 @@ export class ListCommand { if (!Utils.isGuid(options.organizationId)) { return Response.badRequest("`" + options.organizationId + "` is not a GUID."); } - const organization = await this.organizationService.getFromState(options.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizatons) => organizatons.find((o) => o.id == options.organizationId))), + ); if (organization == null) { return Response.error("Organization not found."); } @@ -236,7 +254,12 @@ export class ListCommand { } private async listOrganizations(options: Options) { - let organizations = await firstValueFrom(this.organizationService.memberOrganizations$); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + let organizations = await firstValueFrom(this.organizationService.memberOrganizations$(userId)); if (options.search != null && options.search.trim() !== "") { organizations = CliUtils.searchOrganizations(organizations, options.search); diff --git a/apps/cli/src/models/response.ts b/apps/cli/src/models/response.ts index 76d9509226d..ac0977182f4 100644 --- a/apps/cli/src/models/response.ts +++ b/apps/cli/src/models/response.ts @@ -39,6 +39,10 @@ export class Response { return Response.error("Not found."); } + static noEditPermission(): Response { + return Response.error("You do not have permission to edit this item"); + } + static badRequest(message: string): Response { return Response.error(message); } diff --git a/apps/cli/src/platform/services/cli-sdk-load.service.ts b/apps/cli/src/platform/services/cli-sdk-load.service.ts new file mode 100644 index 00000000000..ee3b48e34d7 --- /dev/null +++ b/apps/cli/src/platform/services/cli-sdk-load.service.ts @@ -0,0 +1,9 @@ +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import * as sdk from "@bitwarden/sdk-internal"; + +export class CliSdkLoadService implements SdkLoadService { + async load(): Promise { + const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); + (sdk as any).init(module); + } +} diff --git a/apps/cli/src/platform/services/node-env-secure-storage.service.ts b/apps/cli/src/platform/services/node-env-secure-storage.service.ts index 2807509e428..5e31995606f 100644 --- a/apps/cli/src/platform/services/node-env-secure-storage.service.ts +++ b/apps/cli/src/platform/services/node-env-secure-storage.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { throwError } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index f57db9909d6..f7dad133f94 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -4,7 +4,7 @@ import * as fs from "fs"; import * as path from "path"; import * as jsdom from "jsdom"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { OrganizationUserApiService, @@ -25,8 +25,8 @@ import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; @@ -36,7 +36,10 @@ import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/aut import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; +import { + AccountServiceImplementation, + getUserId, +} from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; @@ -58,6 +61,8 @@ import { import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; +import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation"; +import { FallbackBulkEncryptService } from "@bitwarden/common/key-management/crypto/services/fallback-bulk-encrypt.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { @@ -65,6 +70,7 @@ import { RegionConfig, } from "@bitwarden/common/platform/abstractions/environment.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; @@ -79,8 +85,6 @@ import { AppIdService } from "@bitwarden/common/platform/services/app-id.service import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; -import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; -import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; @@ -147,7 +151,7 @@ import { ImportApiServiceAbstraction, ImportService, ImportServiceAbstraction, -} from "@bitwarden/importer/core"; +} from "@bitwarden/importer-core"; import { DefaultKdfConfigService, KdfConfigService, @@ -168,6 +172,7 @@ import { import { CliBiometricsService } from "../key-management/cli-biometrics-service"; import { flagEnabled } from "../platform/flags"; import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service"; +import { CliSdkLoadService } from "../platform/services/cli-sdk-load.service"; import { ConsoleLogService } from "../platform/services/console-log.service"; import { I18nService } from "../platform/services/i18n.service"; import { LowdbStorageService } from "../platform/services/lowdb-storage.service"; @@ -238,7 +243,8 @@ export class ServiceContainer { stateService: StateService; autofillSettingsService: AutofillSettingsServiceAbstraction; domainSettingsService: DomainSettingsService; - organizationService: OrganizationService; + organizationService: DefaultOrganizationService; + DefaultOrganizationService: DefaultOrganizationService; providerService: ProviderService; twoFactorService: TwoFactorService; folderApiService: FolderApiService; @@ -266,6 +272,7 @@ export class ServiceContainer { kdfConfigService: KdfConfigService; taskSchedulerService: TaskSchedulerService; sdkService: SdkService; + sdkLoadService: SdkLoadService; cipherAuthorizationService: CipherAuthorizationService; constructor() { @@ -348,6 +355,7 @@ export class ServiceContainer { this.messagingService, this.logService, this.globalStateProvider, + this.singleUserStateProvider, ); this.activeUserStateProvider = new DefaultActiveUserStateProvider( @@ -450,7 +458,7 @@ export class ServiceContainer { this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); - this.organizationService = new OrganizationService(this.stateProvider); + this.organizationService = new DefaultOrganizationService(this.stateProvider); this.policyService = new PolicyService(this.stateProvider, this.organizationService); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( @@ -566,6 +574,7 @@ export class ServiceContainer { const sdkClientFactory = flagEnabled("sdk") ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(); + this.sdkLoadService = new CliSdkLoadService(); this.sdkService = new DefaultSdkService( sdkClientFactory, this.environmentService, @@ -824,6 +833,7 @@ export class ServiceContainer { this.cipherAuthorizationService = new DefaultCipherAuthorizationService( this.collectionService, this.organizationService, + this.accountService, ); } @@ -831,7 +841,7 @@ export class ServiceContainer { this.authService.logOut(() => { /* Do nothing */ }); - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); await Promise.all([ this.eventUploadService.uploadEvents(userId as UserId), this.keyService.clearKeys(), @@ -854,6 +864,7 @@ export class ServiceContainer { return; } + await this.sdkLoadService.load(); await this.storageService.init(); await this.stateService.init(); this.containerService.attachToGlobal(global); diff --git a/apps/cli/src/tools/import.command.ts b/apps/cli/src/tools/import.command.ts index a71c9177d29..f414357c941 100644 --- a/apps/cli/src/tools/import.command.ts +++ b/apps/cli/src/tools/import.command.ts @@ -2,10 +2,16 @@ // @ts-strict-ignore import { OptionValues } from "commander"; import * as inquirer from "inquirer"; +import { firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + OrganizationService, + getOrganizationById, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ImportServiceAbstraction, ImportType } from "@bitwarden/importer/core"; +import { ImportServiceAbstraction, ImportType } from "@bitwarden/importer-core"; import { Response } from "../models/response"; import { MessageResponse } from "../models/response/message.response"; @@ -16,12 +22,19 @@ export class ImportCommand { private importService: ImportServiceAbstraction, private organizationService: OrganizationService, private syncService: SyncService, + private accountService: AccountService, ) {} async run(format: ImportType, filepath: string, options: OptionValues): Promise { const organizationId = options.organizationid; if (organizationId != null) { - const organization = await this.organizationService.getFromState(organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (!userId) { + return Response.badRequest("No user found."); + } + const organization = await firstValueFrom( + this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)), + ); if (organization == null) { return Response.badRequest( diff --git a/apps/cli/src/tools/send/commands/get.command.ts b/apps/cli/src/tools/send/commands/get.command.ts index 0e650c8503d..1d651c50bf0 100644 --- a/apps/cli/src/tools/send/commands/get.command.ts +++ b/apps/cli/src/tools/send/commands/get.command.ts @@ -5,7 +5,7 @@ import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; diff --git a/apps/cli/src/tools/send/commands/receive.command.ts b/apps/cli/src/tools/send/commands/receive.command.ts index 41a6681af55..879d03f6dae 100644 --- a/apps/cli/src/tools/send/commands/receive.command.ts +++ b/apps/cli/src/tools/send/commands/receive.command.ts @@ -5,9 +5,9 @@ import * as inquirer from "inquirer"; import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 3eb0e68de09..f3eb6eef613 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -450,6 +450,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.importService, this.serviceContainer.organizationService, this.serviceContainer.syncService, + this.serviceContainer.accountService, ); const response = await command.run(format, filepath, options); this.processResponse(response); diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index a28d070d19e..28f58187fdb 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -11,10 +11,10 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; import { FolderExport } from "@bitwarden/common/models/export/folder.export"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; @@ -202,7 +202,17 @@ export class CreateCommand { if (orgKey == null) { throw new Error("No encryption key for this organization."); } - const organization = await this.organizationService.get(req.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + if (!userId) { + return Response.badRequest("No user found."); + } + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === req.organizationId))), + ); const currentOrgUserId = organization.organizationUserId; const groups = diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 0668ecacdb4..9d6e3066b29 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -17,7 +17,7 @@ "@bitwarden/auth/common": ["../../libs/auth/src/common"], "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], "@bitwarden/common/*": ["../../libs/common/src/*"], - "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/importer-core": ["../../libs/importer/src"], "@bitwarden/generator-core": ["../../libs/tools/generator/core/src"], "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"], @@ -26,7 +26,6 @@ "../../libs/tools/export/vault-export/vault-export-core/src" ], "@bitwarden/key-management": ["../../libs/key-management/src"], - "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], "@bitwarden/node/*": ["../../libs/node/src/*"] }, "plugins": [ diff --git a/apps/desktop/.eslintrc.json b/apps/desktop/.eslintrc.json deleted file mode 100644 index 5d9ea457c36..00000000000 --- a/apps/desktop/.eslintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "env": { - "browser": true, - "node": true - } -} diff --git a/apps/desktop/desktop_native/.gitignore b/apps/desktop/desktop_native/.gitignore index 1cfa7dafc20..a0a01b28f50 100644 --- a/apps/desktop/desktop_native/.gitignore +++ b/apps/desktop/desktop_native/.gitignore @@ -5,3 +5,4 @@ index.node npm-debug.log* *.node dist +windows_pluginauthenticator_bindings.rs diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index b75b68e0b96..1e1af53d531 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -103,11 +103,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys 0.59.0", ] @@ -332,9 +333,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.84" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", @@ -410,10 +411,30 @@ dependencies = [ ] [[package]] -name = "bitflags" -version = "2.6.0" +name = "bindgen" +version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bitwarden-russh" @@ -426,7 +447,7 @@ dependencies = [ "russh-cryptovec", "ssh-encoding", "ssh-key", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-util", ] @@ -531,7 +552,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -545,13 +566,22 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.7" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -593,10 +623,21 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.23" +name = "clang-sys" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -604,9 +645,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -616,9 +657,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck", "proc-macro2", @@ -699,9 +740,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -789,9 +830,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad7c7515609502d316ab9a24f67dc045132d93bfd3f00713389e90d9898bf30d" +checksum = "0fc894913dccfed0f84106062c284fa021c3ba70cb1d78797d6f5165d4492e45" dependencies = [ "cc", "cxxbridge-cmd", @@ -803,9 +844,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bfd16fca6fd420aebbd80d643c201ee4692114a0de208b790b9cd02ceae65fb" +checksum = "503b2bfb6b3e8ce7f95d865a67419451832083d3186958290cee6c53e39dfcfe" dependencies = [ "cc", "codespan-reporting", @@ -817,9 +858,9 @@ dependencies = [ [[package]] name = "cxxbridge-cmd" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c33fd49f5d956a1b7ee5f7a9768d58580c6752838d92e39d0d56439efdedc35" +checksum = "e0d2cb64a95b4b5a381971482235c4db2e0208302a962acdbe314db03cbbe2fb" dependencies = [ "clap", "codespan-reporting", @@ -830,15 +871,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0f1077278fac36299cce8446effd19fe93a95eedb10d39265f3bf67b3036c9" +checksum = "5f797b0206463c9c2a68ed605ab28892cca784f1ef066050f4942e3de26ad885" [[package]] name = "cxxbridge-macro" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da7e4d6e74af6b79031d264b2f13c3ea70af1978083741c41ffce9308f1f24f" +checksum = "e79010a2093848e65a3e0f7062d3f02fb2ef27f866416dfe436fccfa73d3bb59" dependencies = [ "proc-macro2", "quote", @@ -925,7 +966,7 @@ dependencies = [ "ssh-encoding", "ssh-key", "sysinfo", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tokio-util", @@ -963,7 +1004,7 @@ dependencies = [ "cc", "core-foundation", "glob", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -995,23 +1036,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1077,9 +1118,9 @@ checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enumflags2" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" dependencies = [ "enumflags2_derive", "serde", @@ -1087,9 +1128,9 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", @@ -1120,9 +1161,9 @@ checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -1228,9 +1269,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", @@ -1308,7 +1349,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] [[package]] @@ -1406,9 +1459,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -1445,6 +1498,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" @@ -1524,9 +1586,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" @@ -1540,9 +1602,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "macos_provider" @@ -1608,9 +1670,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] @@ -1622,7 +1684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1893,9 +1955,9 @@ dependencies = [ [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" @@ -2122,9 +2184,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2246,6 +2308,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -2257,9 +2329,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -2309,7 +2381,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -2349,13 +2421,13 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 2.0.11", ] [[package]] @@ -2424,6 +2496,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2435,9 +2513,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", @@ -2454,9 +2532,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "salsa20" @@ -2541,27 +2619,27 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.217" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -2570,9 +2648,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.134" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "itoa", "memchr", @@ -2769,9 +2847,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.95" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -2780,9 +2858,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.32.1" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" dependencies = [ "core-foundation-sys", "libc", @@ -2794,13 +2872,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2830,7 +2908,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -2844,6 +2931,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.37" @@ -2879,9 +2977,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.41.1" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -2895,9 +2993,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -2917,9 +3015,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -3023,9 +3121,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-segmentation" @@ -3197,6 +3295,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wayland-backend" version = "0.3.7" @@ -3405,6 +3512,13 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-plugin-authenticator" +version = "0.0.0" +dependencies = [ + "bindgen", +] + [[package]] name = "windows-registry" version = "0.4.0" @@ -3462,15 +3576,6 @@ dependencies = [ "windows-targets 0.53.0", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -3676,13 +3781,22 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.6.22" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + [[package]] name = "wl-clipboard-rs" version = "0.8.1" @@ -3695,7 +3809,7 @@ dependencies = [ "nix 0.28.0", "os_pipe", "tempfile", - "thiserror", + "thiserror 1.0.69", "tree_magic_mini", "wayland-backend", "wayland-client", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 6230a6bfe15..78142d618ed 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -1,3 +1,13 @@ [workspace] resolver = "2" -members = ["napi", "core", "proxy", "macos_provider"] +members = ["napi", "core", "proxy", "macos_provider", "windows-plugin-authenticator"] + +[workspace.dependencies] +anyhow = "=1.0.94" +log = "=0.4.25" +serde = "=1.0.209" +serde_json = "=1.0.127" +tokio = "=1.43.0" +tokio-util = "=0.7.13" +tokio-stream = "=0.1.15" +thiserror = "=1.0.69" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 170967f555d..03b5a60b8b1 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -13,13 +13,13 @@ default = [ "dep:security-framework", "dep:security-framework-sys", "dep:zbus", - "dep:zbus_polkit" + "dep:zbus_polkit", ] manual_test = [] [dependencies] aes = "=0.8.4" -anyhow = "=1.0.94" +anyhow.workspace = true arboard = { version = "=3.4.1", default-features = false, features = [ "wayland-data-control", ] } @@ -29,10 +29,10 @@ byteorder = "=1.5.0" cbc = { version = "=0.1.2", features = ["alloc"] } homedir = "=0.3.4" pin-project = "=1.1.8" -dirs = "=5.0.1" +dirs = "=6.0.0" futures = "=0.3.31" interprocess = { version = "=2.2.1", features = ["tokio"] } -log = "=0.4.22" +log.workspace = true rand = "=0.8.5" russh-cryptovec = "=0.7.3" scopeguard = "=1.2.0" @@ -45,15 +45,15 @@ ssh-key = { version = "=0.6.7", default-features = false, features = [ "getrandom", ] } bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae" } -tokio = { version = "=1.41.1", features = ["io-util", "sync", "macros", "net"] } -tokio-stream = { version = "=0.1.15", features = ["net"] } -tokio-util = { version = "=0.7.12", features = ["codec"] } -thiserror = "=1.0.69" +tokio = { workspace = true, features = ["io-util", "sync", "macros", "net"] } +tokio-stream = { workspace = true, features = ["net"] } +tokio-util = { workspace = true, features = ["codec"] } +thiserror.workspace = true typenum = "=1.17.0" pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] } rsa = "=0.9.6" ed25519 = { version = "=2.2.3", features = ["pkcs8"] } -sysinfo = { version = "0.32.0", features = ["windows"] } +sysinfo = { version = "=0.33.1", features = ["windows"] } [target.'cfg(windows)'.dependencies] widestring = { version = "=1.1.0", optional = true } diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/macos_provider/Cargo.toml index ff7408d6d44..a488ce88a1c 100644 --- a/apps/desktop/desktop_native/macos_provider/Cargo.toml +++ b/apps/desktop/desktop_native/macos_provider/Cargo.toml @@ -16,15 +16,15 @@ bench = false [dependencies] desktop_core = { path = "../core" } futures = "=0.3.31" -log = "0.4.22" -serde = { version = "1.0.205", features = ["derive"] } -serde_json = "1.0.122" -tokio = { version = "1.39.2", features = ["sync"] } -tokio-util = "0.7.11" -uniffi = { version = "0.28.3", features = ["cli"] } +log.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +tokio = { workspace = true, features = ["sync"] } +tokio-util.workspace = true +uniffi = { version = "=0.28.3", features = ["cli"] } [target.'cfg(target_os = "macos")'.dependencies] -oslog = "0.2.0" +oslog = "=0.2.0" [build-dependencies] -uniffi = { version = "0.28.3", features = ["build"] } +uniffi = { version = "=0.28.3", features = ["build"] } diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 974948e254b..c7ecc766e65 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -20,11 +20,11 @@ anyhow = "=1.0.94" desktop_core = { path = "../core" } napi = { version = "=2.16.13", features = ["async"] } napi-derive = "=2.16.13" -serde = { version = "1.0.209", features = ["derive"] } -serde_json = "1.0.127" -tokio = { version = "=1.41.1" } -tokio-util = "=0.7.12" -tokio-stream = "=0.1.15" +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tokio-stream.workspace = true [target.'cfg(windows)'.dependencies] windows-registry = "=0.4.0" diff --git a/apps/desktop/desktop_native/objc/Cargo.toml b/apps/desktop/desktop_native/objc/Cargo.toml index eae303b9f08..2d6478c77c9 100644 --- a/apps/desktop/desktop_native/objc/Cargo.toml +++ b/apps/desktop/desktop_native/objc/Cargo.toml @@ -9,13 +9,13 @@ publish = false default = [] [dependencies] -anyhow = "=1.0.94" -thiserror = "=1.0.69" -tokio = "1.39.1" +anyhow.workspace = true +thiserror.workspace = true +tokio.workspace = true [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "=0.10.0" [build-dependencies] -cc = "1.2.4" -glob = "0.3.2" +cc = "=1.2.4" +glob = "=0.3.2" diff --git a/apps/desktop/desktop_native/proxy/Cargo.toml b/apps/desktop/desktop_native/proxy/Cargo.toml index 3618a11a921..27f2856f3a6 100644 --- a/apps/desktop/desktop_native/proxy/Cargo.toml +++ b/apps/desktop/desktop_native/proxy/Cargo.toml @@ -7,13 +7,13 @@ version = "0.0.0" publish = false [dependencies] -anyhow = "=1.0.94" +anyhow.workspace = true desktop_core = { path = "../core" } futures = "=0.3.31" -log = "=0.4.22" +log.workspace = true simplelog = "=0.12.2" -tokio = { version = "=1.41.1", features = ["io-std", "io-util", "macros", "rt"] } -tokio-util = { version = "=0.7.12", features = ["codec"] } +tokio = { workspace = true, features = ["io-std", "io-util", "macros", "rt"] } +tokio-util = { workspace = true, features = ["codec"] } [target.'cfg(target_os = "macos")'.dependencies] embed_plist = "=1.2.2" diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/Cargo.toml b/apps/desktop/desktop_native/windows-plugin-authenticator/Cargo.toml new file mode 100644 index 00000000000..b8759cfca3f --- /dev/null +++ b/apps/desktop/desktop_native/windows-plugin-authenticator/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "windows-plugin-authenticator" +version = "0.0.0" +edition = "2021" +license = "GPL-3.0" +publish = false + +[target.'cfg(target_os = "windows")'.build-dependencies] +bindgen = "0.71.1" diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/README.md b/apps/desktop/desktop_native/windows-plugin-authenticator/README.md new file mode 100644 index 00000000000..6dc72ceed46 --- /dev/null +++ b/apps/desktop/desktop_native/windows-plugin-authenticator/README.md @@ -0,0 +1,23 @@ +# windows-plugin-authenticator + +This is an internal crate that's meant to be a safe abstraction layer over the generated Rust bindings for the Windows WebAuthn Plugin Authenticator API's. + +You can find more information about the Windows WebAuthn API's [here](https://github.com/microsoft/webauthn). + +## Building + +To build this crate, set the following environment variables: + +- `LIBCLANG_PATH` -> the path to the `bin` directory of your LLVM install ([more info](https://rust-lang.github.io/rust-bindgen/requirements.html?highlight=libclang_path#installing-clang)) + +### Bash Example + +``` +export LIBCLANG_PATH='C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin' +``` + +### PowerShell Example + +``` +$env:LIBCLANG_PATH = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin' +``` diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/build.rs b/apps/desktop/desktop_native/windows-plugin-authenticator/build.rs new file mode 100644 index 00000000000..7bc311fb12d --- /dev/null +++ b/apps/desktop/desktop_native/windows-plugin-authenticator/build.rs @@ -0,0 +1,22 @@ +fn main() { + #[cfg(target_os = "windows")] + windows(); +} + +#[cfg(target_os = "windows")] +fn windows() { + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); + + let bindings = bindgen::Builder::default() + .header("pluginauthenticator.hpp") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate() + .expect("Unable to generate bindings."); + + bindings + .write_to_file(format!( + "{}\\windows_pluginauthenticator_bindings.rs", + out_dir + )) + .expect("Couldn't write bindings."); +} diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/pluginauthenticator.hpp b/apps/desktop/desktop_native/windows-plugin-authenticator/pluginauthenticator.hpp new file mode 100644 index 00000000000..c800266a3e6 --- /dev/null +++ b/apps/desktop/desktop_native/windows-plugin-authenticator/pluginauthenticator.hpp @@ -0,0 +1,231 @@ +/* + Bitwarden's pluginauthenticator.hpp + + Source: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h + + This is a C++ header file, so the extension has been manually + changed from `.h` to `.hpp`, so bindgen will automatically + generate the correct C++ bindings. + + More Info: https://rust-lang.github.io/rust-bindgen/cpp.html +*/ + +/* this ALWAYS GENERATED file contains the definitions for the interfaces */ + +/* File created by MIDL compiler version 8.01.0628 */ +/* @@MIDL_FILE_HEADING( ) */ + +/* verify that the version is high enough to compile this file*/ +#ifndef __REQUIRED_RPCNDR_H_VERSION__ +#define __REQUIRED_RPCNDR_H_VERSION__ 501 +#endif + +/* verify that the version is high enough to compile this file*/ +#ifndef __REQUIRED_RPCSAL_H_VERSION__ +#define __REQUIRED_RPCSAL_H_VERSION__ 100 +#endif + +#include "rpc.h" +#include "rpcndr.h" + +#ifndef __RPCNDR_H_VERSION__ +#error this stub requires an updated version of +#endif /* __RPCNDR_H_VERSION__ */ + +#ifndef COM_NO_WINDOWS_H +#include "windows.h" +#include "ole2.h" +#endif /*COM_NO_WINDOWS_H*/ + +#ifndef __pluginauthenticator_h__ +#define __pluginauthenticator_h__ + +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +#pragma once +#endif + +#ifndef DECLSPEC_XFGVIRT +#if defined(_CONTROL_FLOW_GUARD_XFG) +#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func)) +#else +#define DECLSPEC_XFGVIRT(base, func) +#endif +#endif + +/* Forward Declarations */ + +#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ +#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ +typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator; + +#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */ + +/* header files for imported files */ +#include "oaidl.h" +#include "webauthn.h" + +#ifdef __cplusplus +extern "C"{ +#endif + +/* interface __MIDL_itf_pluginauthenticator_0000_0000 */ +/* [local] */ + +typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST + { + HWND hWnd; + GUID transactionId; + DWORD cbRequestSignature; + /* [size_is] */ byte *pbRequestSignature; + DWORD cbEncodedRequest; + /* [size_is] */ byte *pbEncodedRequest; + } EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST; + +typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST; + +typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST; + +typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE + { + DWORD cbEncodedResponse; + /* [size_is] */ byte *pbEncodedResponse; + } EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE; + +typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE; + +typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE; + +typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST + { + GUID transactionId; + DWORD cbRequestSignature; + /* [size_is] */ byte *pbRequestSignature; + } EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; + +typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; + +typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; + +extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec; +extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec; + +#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ +#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ + +/* interface EXPERIMENTAL_IPluginAuthenticator */ +/* [unique][version][uuid][object] */ + +EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator; + +#if defined(__cplusplus) && !defined(CINTERFACE) + + MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998") + EXPERIMENTAL_IPluginAuthenticator : public IUnknown + { + public: + virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential( + /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, + /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0; + + virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion( + /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, + /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0; + + virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation( + /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0; + + }; + +#else /* C style interface */ + + typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl + { + BEGIN_INTERFACE + + DECLSPEC_XFGVIRT(IUnknown, QueryInterface) + HRESULT ( STDMETHODCALLTYPE *QueryInterface )( + __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, + /* [in] */ __RPC__in REFIID riid, + /* [annotation][iid_is][out] */ + _COM_Outptr_ void **ppvObject); + + DECLSPEC_XFGVIRT(IUnknown, AddRef) + ULONG ( STDMETHODCALLTYPE *AddRef )( + __RPC__in EXPERIMENTAL_IPluginAuthenticator * This); + + DECLSPEC_XFGVIRT(IUnknown, Release) + ULONG ( STDMETHODCALLTYPE *Release )( + __RPC__in EXPERIMENTAL_IPluginAuthenticator * This); + + DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential) + HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )( + __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, + /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, + /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response); + + DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion) + HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )( + __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, + /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, + /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response); + + DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation) + HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )( + __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, + /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request); + + END_INTERFACE + } EXPERIMENTAL_IPluginAuthenticatorVtbl; + + interface EXPERIMENTAL_IPluginAuthenticator + { + CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl; + }; + +#ifdef COBJMACROS + + +#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \ + ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) + +#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \ + ( (This)->lpVtbl -> AddRef(This) ) + +#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \ + ( (This)->lpVtbl -> Release(This) ) + + +#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \ + ( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) ) + +#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \ + ( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) ) + +#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \ + ( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) ) + +#endif /* COBJMACROS */ + +#endif /* C style interface */ + +#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */ + +/* Additional Prototypes for ALL interfaces */ + +unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * ); +unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * ); +unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * ); +void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * ); + +unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * ); +unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * ); +unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * ); +void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * ); + +/* end of Additional Prototypes */ + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/src/lib.rs b/apps/desktop/desktop_native/windows-plugin-authenticator/src/lib.rs new file mode 100644 index 00000000000..e226000e6fa --- /dev/null +++ b/apps/desktop/desktop_native/windows-plugin-authenticator/src/lib.rs @@ -0,0 +1,11 @@ +#![cfg(target_os = "windows")] + +mod pa; + +pub fn get_version_number() -> u64 { + unsafe { pa::WebAuthNGetApiVersionNumber() }.into() +} + +pub fn add_authenticator() { + unimplemented!(); +} diff --git a/apps/desktop/desktop_native/windows-plugin-authenticator/src/pa.rs b/apps/desktop/desktop_native/windows-plugin-authenticator/src/pa.rs new file mode 100644 index 00000000000..3da5a77a243 --- /dev/null +++ b/apps/desktop/desktop_native/windows-plugin-authenticator/src/pa.rs @@ -0,0 +1,15 @@ +/* + The 'pa' (plugin authenticator) module will contain the generated + bindgen code. + + The attributes below will suppress warnings from the generated code. +*/ + +#![cfg(target_os = "windows")] +#![allow(clippy::all)] +#![allow(warnings)] + +include!(concat!( + env!("OUT_DIR"), + "/windows_pluginauthenticator_bindings.rs" +)); diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 9d874497386..e807b40c48a 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -20,7 +20,7 @@ "**/node_modules/@bitwarden/desktop-napi/index.js", "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" ], - "electronVersion": "33.3.1", + "electronVersion": "34.0.0", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", diff --git a/apps/desktop/native-messaging-test-runner/.eslintrc.json b/apps/desktop/native-messaging-test-runner/.eslintrc.json deleted file mode 100644 index d5ba8f9d9ca..00000000000 --- a/apps/desktop/native-messaging-test-runner/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-console": "off" - } -} diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index f727c903a7f..f9c5f0709e4 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -18,7 +18,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "22.10.5", + "@types/node": "22.10.7", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" } @@ -106,9 +106,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index b7da729d7d1..977b93e70d2 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -23,7 +23,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "22.10.5", + "@types/node": "22.10.7", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, diff --git a/apps/desktop/native-messaging-test-runner/src/ipc.service.ts b/apps/desktop/native-messaging-test-runner/src/ipc.service.ts index 8513363956e..68c7ac73ab0 100644 --- a/apps/desktop/native-messaging-test-runner/src/ipc.service.ts +++ b/apps/desktop/native-messaging-test-runner/src/ipc.service.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { homedir } from "os"; diff --git a/apps/desktop/native-messaging-test-runner/src/native-message.service.ts b/apps/desktop/native-messaging-test-runner/src/native-message.service.ts index 94fdde026b2..c01d581afe8 100644 --- a/apps/desktop/native-messaging-test-runner/src/native-message.service.ts +++ b/apps/desktop/native-messaging-test-runner/src/native-message.service.ts @@ -1,12 +1,13 @@ +/* eslint-disable no-console */ import "module-alias/register"; import { v4 as uuidv4 } from "uuid"; +import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; -import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; // eslint-disable-next-line no-restricted-imports diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist index 5bb95f76afb..bf67e8b698a 100644 --- a/apps/desktop/resources/entitlements.mas.plist +++ b/apps/desktop/resources/entitlements.mas.plist @@ -2,20 +2,23 @@ - com.apple.application-identifier +com.apple.application-identifier +LTZ2PFU5D6.com.bitwarden.desktop +com.apple.developer.team-identifier +LTZ2PFU5D6 +com.apple.security.app-sandbox + +com.apple.security.application-groups + LTZ2PFU5D6.com.bitwarden.desktop - com.apple.developer.team-identifier - LTZ2PFU5D6 - com.apple.security.app-sandbox - - com.apple.security.application-groups - - LTZ2PFU5D6.com.bitwarden.desktop - - com.apple.security.network.client - - com.apple.security.files.user-selected.read-write - + +com.apple.security.network.client + +com.apple.security.files.user-selected.read-write + +com.apple.security.device.usb + + -
@@ -14,15 +12,11 @@
-

{{ "loginInitiated" | i18n }}

+

{{ "logInRequestSent" | i18n }}

-
-

{{ "notificationSentDevice" | i18n }}

- -

- {{ "fingerprintMatchInfo" | i18n }} -

-
+

+ {{ "notificationSentDeviceComplete" | i18n }} +

{{ "fingerprintPhraseHeader" | i18n }}

@@ -39,7 +33,7 @@
-
+
{{ "loginWithDeviceEnabledNote" | i18n }} {{ "viewAllLoginOptions" | i18n }}
@@ -52,7 +46,7 @@ >

{{ "adminApprovalRequested" | i18n }}

-
+

{{ "adminApprovalRequestSentToAdmins" | i18n }}

{{ "youWillBeNotifiedOnceApproved" | i18n }}

@@ -66,7 +60,7 @@
-
+
{{ "troubleLoggingIn" | i18n }} {{ "viewAllLoginOptions" | i18n }}
diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index b3709a15882..f613bdc7ddc 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -12,7 +12,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { FakeGlobalState } from "@bitwarden/common/spec/fake-state"; diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts index a964d676159..22a79ef3696 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -16,7 +16,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; diff --git a/apps/web/src/app/auth/register-form/register-form.component.html b/apps/web/src/app/auth/register-form/register-form.component.html deleted file mode 100644 index 19a7a95b298..00000000000 --- a/apps/web/src/app/auth/register-form/register-form.component.html +++ /dev/null @@ -1,158 +0,0 @@ - - - -
-
- - {{ "emailAddress" | i18n }} - - {{ "emailAddressDesc" | i18n }} - -
- -
- - {{ "name" | i18n }} - - {{ "yourNameDesc" | i18n }} - -
- -
- - - - {{ "masterPass" | i18n }} - - - - {{ "important" | i18n }} - {{ "masterPassImportant" | i18n }} {{ characterMinimumMessage }} - - - - -
- -
- - {{ "reTypeMasterPass" | i18n }} - - - -
- -
- - {{ "masterPassHintLabel" | i18n }} - - {{ "masterPassHintDesc" | i18n }} - -
- -
- -
-
- - {{ "checkForBreaches" | i18n }} -
-
- - - - {{ "acceptPolicies" | i18n }}
- {{ - "termsOfService" | i18n - }}, - {{ - "privacyPolicy" | i18n - }} -
-
- -
- - - - - - -
-

- {{ "alreadyHaveAccount" | i18n }} - {{ "logIn" | i18n }} -

- -
- diff --git a/apps/web/src/app/auth/register-form/register-form.component.ts b/apps/web/src/app/auth/register-form/register-form.component.ts deleted file mode 100644 index 7d3e6dbd00e..00000000000 --- a/apps/web/src/app/auth/register-form/register-form.component.ts +++ /dev/null @@ -1,121 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Input, OnInit } from "@angular/core"; -import { UntypedFormBuilder } from "@angular/forms"; -import { Router } from "@angular/router"; - -import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/auth/components/register.component"; -import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -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 { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; -import { RegisterRequest } from "@bitwarden/common/models/request/register.request"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { KeyService } from "@bitwarden/key-management"; - -import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; - -@Component({ - selector: "app-register-form", - templateUrl: "./register-form.component.html", -}) -export class RegisterFormComponent extends BaseRegisterComponent implements OnInit { - @Input() queryParamEmail: string; - @Input() queryParamFromOrgInvite: boolean; - @Input() enforcedPolicyOptions: MasterPasswordPolicyOptions; - @Input() referenceDataValue: ReferenceEventRequest; - - showErrorSummary = false; - characterMinimumMessage: string; - - constructor( - formValidationErrorService: FormValidationErrorsService, - formBuilder: UntypedFormBuilder, - loginStrategyService: LoginStrategyServiceAbstraction, - router: Router, - i18nService: I18nService, - keyService: KeyService, - apiService: ApiService, - stateService: StateService, - platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - private policyService: PolicyService, - environmentService: EnvironmentService, - logService: LogService, - auditService: AuditService, - dialogService: DialogService, - acceptOrgInviteService: AcceptOrganizationInviteService, - toastService: ToastService, - ) { - super( - formValidationErrorService, - formBuilder, - loginStrategyService, - router, - i18nService, - keyService, - apiService, - stateService, - platformUtilsService, - passwordGenerationService, - environmentService, - logService, - auditService, - dialogService, - toastService, - ); - this.modifyRegisterRequest = async (request: RegisterRequest) => { - // Org invites are deep linked. Non-existent accounts are redirected to the register page. - // Org user id and token are included here only for validation and two factor purposes. - const orgInvite = await acceptOrgInviteService.getOrganizationInvite(); - if (orgInvite != null) { - request.organizationUserId = orgInvite.organizationUserId; - request.token = orgInvite.token; - } - // Invite is accepted after login (on deep link redirect). - }; - } - - async ngOnInit() { - await super.ngOnInit(); - this.referenceData = this.referenceDataValue; - if (this.queryParamEmail) { - this.formGroup.get("email")?.setValue(this.queryParamEmail); - } - - if (this.enforcedPolicyOptions != null && this.enforcedPolicyOptions.minLength > 0) { - this.characterMinimumMessage = ""; - } else { - this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength); - } - } - - async submit() { - if ( - this.enforcedPolicyOptions != null && - !this.policyService.evaluateMasterPassword( - this.passwordStrengthResult.score, - this.formGroup.value.masterPassword, - this.enforcedPolicyOptions, - ) - ) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), - }); - return; - } - - await super.submit(false); - } -} diff --git a/apps/web/src/app/auth/register-form/register-form.module.ts b/apps/web/src/app/auth/register-form/register-form.module.ts deleted file mode 100644 index b63cb18506d..00000000000 --- a/apps/web/src/app/auth/register-form/register-form.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NgModule } from "@angular/core"; - -import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; - -import { SharedModule } from "../../shared"; - -import { RegisterFormComponent } from "./register-form.component"; - -@NgModule({ - imports: [SharedModule, PasswordCalloutComponent], - declarations: [RegisterFormComponent], - exports: [RegisterFormComponent], -}) -export class RegisterFormModule {} diff --git a/apps/web/src/app/auth/settings/account/account.component.html b/apps/web/src/app/auth/settings/account/account.component.html index 4055f14219c..9f405c65083 100644 --- a/apps/web/src/app/auth/settings/account/account.component.html +++ b/apps/web/src/app/auth/settings/account/account.component.html @@ -9,6 +9,26 @@
+ + + + @@ -32,7 +52,6 @@ - diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts index 7e1be937a22..c32e2c375b2 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -1,11 +1,19 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { combineLatest, from, lastValueFrom, map, Observable } from "rxjs"; +import { Component, OnInit, OnDestroy } from "@angular/core"; +import { + combineLatest, + firstValueFrom, + from, + lastValueFrom, + map, + Observable, + Subject, + takeUntil, +} from "rxjs"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { DialogService } from "@bitwarden/components"; @@ -14,21 +22,23 @@ import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.compone import { DeauthorizeSessionsComponent } from "./deauthorize-sessions.component"; import { DeleteAccountDialogComponent } from "./delete-account-dialog.component"; +import { SetAccountVerifyDevicesDialogComponent } from "./set-account-verify-devices-dialog.component"; @Component({ selector: "app-account", templateUrl: "account.component.html", }) -export class AccountComponent implements OnInit { - @ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true }) - deauthModalRef: ViewContainerRef; +export class AccountComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); - showChangeEmail$: Observable; - showPurgeVault$: Observable; - showDeleteAccount$: Observable; + showChangeEmail$: Observable = new Observable(); + showPurgeVault$: Observable = new Observable(); + showDeleteAccount$: Observable = new Observable(); + showSetNewDeviceLoginProtection$: Observable = new Observable(); + verifyNewDeviceLogin: boolean = true; constructor( - private modalService: ModalService, + private accountService: AccountService, private dialogService: DialogService, private userVerificationService: UserVerificationService, private configService: ConfigService, @@ -36,13 +46,20 @@ export class AccountComponent implements OnInit { ) {} async ngOnInit() { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + this.showSetNewDeviceLoginProtection$ = this.configService.getFeatureFlag$( + FeatureFlag.NewDeviceVerification, + ); const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.AccountDeprovisioning, ); - const userIsManagedByOrganization$ = this.organizationService.organizations$.pipe( - map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)), - ); + const userIsManagedByOrganization$ = this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)), + ); const hasMasterPassword$ = from(this.userVerificationService.hasMasterPassword()); @@ -76,11 +93,17 @@ export class AccountComponent implements OnInit { !isAccountDeprovisioningEnabled || !userIsManagedByOrganization, ), ); + this.accountService.accountVerifyNewDeviceLogin$ + .pipe(takeUntil(this.destroy$)) + .subscribe((verifyDevices) => { + this.verifyNewDeviceLogin = verifyDevices; + }); } - async deauthorizeSessions() { - await this.modalService.openViewRef(DeauthorizeSessionsComponent, this.deauthModalRef); - } + deauthorizeSessions = async () => { + const dialogRef = DeauthorizeSessionsComponent.open(this.dialogService); + await lastValueFrom(dialogRef.closed); + }; purgeVault = async () => { const dialogRef = PurgeVaultComponent.open(this.dialogService); @@ -91,4 +114,14 @@ export class AccountComponent implements OnInit { const dialogRef = DeleteAccountDialogComponent.open(this.dialogService); await lastValueFrom(dialogRef.closed); }; + + setNewDeviceLoginProtection = async () => { + const dialogRef = SetAccountVerifyDevicesDialogComponent.open(this.dialogService); + await lastValueFrom(dialogRef.closed); + }; + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html index 34e9f734fc0..7e8b69a0c48 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html @@ -28,16 +28,16 @@ '!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600': customColorSelected, }" - class="tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600" + class="tw-relative tw-flex tw-size-24 tw-cursor-pointer tw-place-content-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600" [style.background-color]="customColor$ | async" > {{ "dangerZone" | i18n }}
-

- {{ - (accountDeprovisioningEnabled$ | async) && content.children.length === 1 - ? ("dangerZoneDescSingular" | i18n) - : ("dangerZoneDesc" | i18n) - }} -

-
diff --git a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.html b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.html index 3867e9d1ca7..ecadacfeed2 100644 --- a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.html +++ b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.html @@ -1,38 +1,21 @@ - +
+ + +

{{ "deauthorizeSessionsDesc" | i18n }}

+ {{ "deauthorizeSessionsWarning" | i18n }} + +
+ + + + +
+
diff --git a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts index 57ca0e0ecfc..a7c466d4ffc 100644 --- a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts +++ b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts @@ -1,6 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; @@ -8,33 +7,33 @@ import { Verification } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-deauthorize-sessions", templateUrl: "deauthorize-sessions.component.html", }) export class DeauthorizeSessionsComponent { - masterPassword: Verification; - formPromise: Promise; + deauthForm = this.formBuilder.group({ + verification: undefined as Verification | undefined, + }); + invalidSecret: boolean = false; constructor( private apiService: ApiService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, + private formBuilder: FormBuilder, private userVerificationService: UserVerificationService, private messagingService: MessagingService, private logService: LogService, private toastService: ToastService, ) {} - async submit() { + submit = async () => { try { - this.formPromise = this.userVerificationService - .buildRequest(this.masterPassword) - .then((request) => this.apiService.postSecurityStamp(request)); - await this.formPromise; + const verification: Verification = this.deauthForm.value.verification!; + const request = await this.userVerificationService.buildRequest(verification); + await this.apiService.postSecurityStamp(request); this.toastService.showToast({ variant: "success", title: this.i18nService.t("sessionsDeauthorized"), @@ -44,5 +43,9 @@ export class DeauthorizeSessionsComponent { } catch (e) { this.logService.error(e); } + }; + + static open(dialogService: DialogService) { + return dialogService.open(DeauthorizeSessionsComponent); } } diff --git a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts index aa5cfa3c1dc..64d7dc1b0da 100644 --- a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts @@ -8,7 +8,6 @@ import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-a import { Verification } from "@bitwarden/common/auth/types/verification"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; @Component({ @@ -22,7 +21,6 @@ export class DeleteAccountDialogComponent { constructor( private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, private accountApiService: AccountApiService, private dialogRef: DialogRef, diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index 4f4920270f0..72731363806 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -9,6 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ProfileResponse } from "@bitwarden/common/models/response/profile.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -49,16 +50,21 @@ export class ProfileComponent implements OnInit, OnDestroy { this.fingerprintMaterial = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.managingOrganization$ = this.configService .getFeatureFlag$(FeatureFlag.AccountDeprovisioning) .pipe( switchMap((isAccountDeprovisioningEnabled) => isAccountDeprovisioningEnabled - ? this.organizationService.organizations$.pipe( - map((organizations) => - organizations.find((o) => o.userIsManagedByOrganization === true), - ), - ) + ? this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => + organizations.find((o) => o.userIsManagedByOrganization === true), + ), + ) : of(null), ), ); diff --git a/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.html b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.html new file mode 100644 index 00000000000..6cd5bbf9212 --- /dev/null +++ b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.html @@ -0,0 +1,43 @@ +
+ + +

+ {{ "turnOffNewDeviceLoginProtectionModalDesc" | i18n }} +

+

+ {{ "turnOnNewDeviceLoginProtectionModalDesc" | i18n }} +

+ {{ + "turnOffNewDeviceLoginProtectionWarning" | i18n + }} + +
+ + + + + +
+
diff --git a/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts new file mode 100644 index 00000000000..dc7735d7520 --- /dev/null +++ b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts @@ -0,0 +1,122 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { firstValueFrom, Subject, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { SetVerifyDevicesRequest } from "@bitwarden/common/auth/models/request/set-verify-devices.request"; +import { Verification } from "@bitwarden/common/auth/types/verification"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + AsyncActionsModule, + ButtonModule, + CalloutModule, + DialogModule, + DialogService, + FormFieldModule, + IconButtonModule, + RadioButtonModule, + SelectModule, + ToastService, +} from "@bitwarden/components"; + +@Component({ + templateUrl: "./set-account-verify-devices-dialog.component.html", + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + JslibModule, + FormFieldModule, + AsyncActionsModule, + ButtonModule, + IconButtonModule, + SelectModule, + CalloutModule, + RadioButtonModule, + DialogModule, + UserVerificationFormInputComponent, + ], +}) +export class SetAccountVerifyDevicesDialogComponent implements OnInit, OnDestroy { + // use this subject for all subscriptions to ensure all subscripts are completed + private destroy$ = new Subject(); + // the default for new device verification is true + verifyNewDeviceLogin: boolean = true; + has2faConfigured: boolean = false; + + setVerifyDevicesForm = this.formBuilder.group({ + verification: undefined as Verification | undefined, + }); + invalidSecret: boolean = false; + + constructor( + private i18nService: I18nService, + private formBuilder: FormBuilder, + private accountApiService: AccountApiService, + private accountService: AccountService, + private userVerificationService: UserVerificationService, + private dialogRef: DialogRef, + private toastService: ToastService, + private apiService: ApiService, + ) { + this.accountService.accountVerifyNewDeviceLogin$ + .pipe(takeUntil(this.destroy$)) + .subscribe((verifyDevices: boolean) => { + this.verifyNewDeviceLogin = verifyDevices; + }); + } + + async ngOnInit() { + const twoFactorProviders = await this.apiService.getTwoFactorProviders(); + this.has2faConfigured = twoFactorProviders.data.length > 0; + } + + submit = async () => { + try { + const activeAccount = await firstValueFrom( + this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)), + ); + const verification: Verification = this.setVerifyDevicesForm.value.verification!; + const request: SetVerifyDevicesRequest = await this.userVerificationService.buildRequest( + verification, + SetVerifyDevicesRequest, + ); + // set verify device opposite what is currently is. + request.verifyDevices = !this.verifyNewDeviceLogin; + await this.accountApiService.setVerifyDevices(request); + await this.accountService.setAccountVerifyNewDeviceLogin( + activeAccount!.id, + request.verifyDevices, + ); + this.dialogRef.close(); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("accountNewDeviceLoginProtectionSaved"), + }); + } catch (e) { + if (e instanceof ErrorResponse && e.statusCode === 400) { + this.invalidSecret = true; + } + throw e; + } + }; + + static open(dialogService: DialogService) { + return dialogService.open(SetAccountVerifyDevicesDialogComponent); + } + + // closes subscription leaks + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 8f0f195440a..dd5c56f9b91 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -12,10 +12,10 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserId } from "@bitwarden/common/types/guid"; @@ -23,7 +23,6 @@ import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { UserKeyRotationService } from "../../key-management/key-rotation/user-key-rotation.service"; @@ -46,8 +45,6 @@ export class ChangePasswordComponent i18nService: I18nService, keyService: KeyService, messagingService: MessagingService, - stateService: StateService, - passwordGenerationService: PasswordGenerationServiceAbstraction, platformUtilsService: PlatformUtilsService, policyService: PolicyService, private auditService: AuditService, @@ -67,10 +64,8 @@ export class ChangePasswordComponent i18nService, keyService, messagingService, - passwordGenerationService, platformUtilsService, policyService, - stateService, dialogService, kdfConfigService, masterPasswordService, @@ -188,7 +183,7 @@ export class ChangePasswordComponent await this.kdfConfigService.getKdfConfig(), ); - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const newLocalKeyHash = await this.keyService.hashMasterKey( this.masterPassword, newMasterKey, diff --git a/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts b/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts index 75d43bb3bc7..73191e1539e 100644 --- a/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts @@ -4,7 +4,7 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/ang import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index 73e32add5c2..83bdfffbe4f 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -8,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -83,7 +84,8 @@ export class EmergencyAccessComponent implements OnInit { } async ngOnInit() { - const orgs = await this.organizationService.getAll(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); this.isOrganizationOwner = orgs.some((o) => o.isOwner); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index 4e00c962ffd..5747386cf84 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -13,9 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfType, KdfConfigService, KeyService } from "@bitwarden/key-management"; import { EmergencyAccessService } from "../../../emergency-access"; @@ -53,8 +51,6 @@ export class EmergencyAccessTakeoverComponent i18nService: I18nService, keyService: KeyService, messagingService: MessagingService, - stateService: StateService, - passwordGenerationService: PasswordGenerationServiceAbstraction, platformUtilsService: PlatformUtilsService, policyService: PolicyService, private emergencyAccessService: EmergencyAccessService, @@ -70,10 +66,8 @@ export class EmergencyAccessTakeoverComponent i18nService, keyService, messagingService, - passwordGenerationService, platformUtilsService, policyService, - stateService, dialogService, kdfConfigService, masterPasswordService, diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts index 0ad7eef81be..c5114c0be6a 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts @@ -43,6 +43,7 @@ describe("EmergencyViewDialogComponent", () => { imports: [EmergencyViewDialogComponent, NoopAnimationsModule], providers: [ { provide: OrganizationService, useValue: mock() }, + { provide: AccountService, useValue: accountService }, { provide: CollectionService, useValue: mock() }, { provide: FolderService, useValue: mock() }, { provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } }, diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index 37827a33afe..2b8d8be3d0f 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -5,7 +5,7 @@
diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html index 0a2eb346b1b..c9e2e111481 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html @@ -14,10 +14,7 @@ {{ "twoStepLoginProviderEnabled" | i18n }} -

{{ "twoFactorWebAuthnWarning" | i18n }}

-
    -
  • {{ "twoFactorWebAuthnSupportWeb" | i18n }}
  • -
+

{{ "twoFactorWebAuthnWarning1" | i18n }}

FIDO2 WebAuthn logo
    diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index 3b20718873d..4530692ebee 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DialogRef } from "@angular/cdk/dialog"; -import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { first, firstValueFrom, @@ -14,7 +14,6 @@ import { } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -67,11 +66,10 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { constructor( protected dialogService: DialogService, protected apiService: ApiService, - protected modalService: ModalService, protected messagingService: MessagingService, protected policyService: PolicyService, billingAccountProfileStateService: BillingAccountProfileStateService, - private accountService: AccountService, + protected accountService: AccountService, ) { this.canAccessPremium$ = this.accountService.activeAccount$.pipe( switchMap((account) => @@ -268,13 +266,6 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { return type === TwoFactorProviderType.OrganizationDuo; } - protected async openModal(ref: ViewContainerRef, type: Type): Promise { - const [modal, childComponent] = await this.modalService.openViewRef(type, ref); - this.modal = modal; - - return childComponent; - } - protected updateStatus(enabled: boolean, type: TwoFactorProviderType) { if (!enabled && this.modal != null) { this.modal.close(); diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts index 48a7f40d238..c12d7941c44 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts @@ -10,6 +10,8 @@ import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstract import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view"; import { Verification } from "@bitwarden/common/auth/types/verification"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DialogService } from "@bitwarden/components/src/dialog/dialog.service"; import { WebauthnLoginAdminService } from "../../../core/services/webauthn-login/webauthn-login-admin.service"; diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html deleted file mode 100644 index 077836a7634..00000000000 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html +++ /dev/null @@ -1,149 +0,0 @@ - - - -
    -

    {{ "createAccount" | i18n }}

    -
    - -
    -
    -
    -
    -
    -
    - Bitwarden - -
    - - - - - - - - - - - - - - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -

    - {{ freeTrialText }} -

    - -
    - - - - - - - - - - - - - - -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts deleted file mode 100644 index 61fc7a60035..00000000000 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts +++ /dev/null @@ -1,336 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { StepperSelectionEvent } from "@angular/cdk/stepper"; -import { TitleCasePipe } from "@angular/common"; -import { NO_ERRORS_SCHEMA } from "@angular/core"; -import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; -import { FormBuilder, UntypedFormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { RouterTestingModule } from "@angular/router/testing"; -import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; - -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.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 { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PlanType } from "@bitwarden/common/billing/enums"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; - -import { RouterService } from "../../core"; -import { SharedModule } from "../../shared"; -import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; -import { OrganizationInvite } from "../organization-invite/organization-invite"; - -import { TrialInitiationComponent } from "./trial-initiation.component"; -import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component"; - -describe("TrialInitiationComponent", () => { - let component: TrialInitiationComponent; - let fixture: ComponentFixture; - const mockQueryParams = new BehaviorSubject({ org: "enterprise" }); - const testOrgId = "91329456-5b9f-44b3-9279-6bb9ee6a0974"; - const formBuilder: FormBuilder = new FormBuilder(); - let routerSpy: jest.SpyInstance; - - let stateServiceMock: MockProxy; - let policyApiServiceMock: MockProxy; - let policyServiceMock: MockProxy; - let routerServiceMock: MockProxy; - let acceptOrgInviteServiceMock: MockProxy; - let organizationBillingServiceMock: MockProxy; - let configServiceMock: MockProxy; - - beforeEach(() => { - // only define services directly that we want to mock return values in this component - stateServiceMock = mock(); - policyApiServiceMock = mock(); - policyServiceMock = mock(); - routerServiceMock = mock(); - acceptOrgInviteServiceMock = mock(); - organizationBillingServiceMock = mock(); - configServiceMock = mock(); - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - TestBed.configureTestingModule({ - imports: [ - SharedModule, - RouterTestingModule.withRoutes([ - { path: "trial", component: TrialInitiationComponent }, - { - path: `organizations/${testOrgId}/vault`, - component: BlankComponent, - }, - { - path: `organizations/${testOrgId}/members`, - component: BlankComponent, - }, - ]), - ], - declarations: [TrialInitiationComponent, I18nPipe], - providers: [ - UntypedFormBuilder, - { - provide: ActivatedRoute, - useValue: { - queryParams: mockQueryParams.asObservable(), - }, - }, - { provide: StateService, useValue: stateServiceMock }, - { provide: PolicyService, useValue: policyServiceMock }, - { provide: PolicyApiServiceAbstraction, useValue: policyApiServiceMock }, - { provide: LogService, useValue: mock() }, - { provide: I18nService, useValue: mock() }, - { provide: TitleCasePipe, useValue: mock() }, - { - provide: VerticalStepperComponent, - useClass: VerticalStepperStubComponent, - }, - { - provide: RouterService, - useValue: routerServiceMock, - }, - { - provide: AcceptOrganizationInviteService, - useValue: acceptOrgInviteServiceMock, - }, - { - provide: OrganizationBillingService, - useValue: organizationBillingServiceMock, - }, - { - provide: ConfigService, - useValue: configServiceMock, - }, - ], - schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component) - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); - - // These tests demonstrate mocking service calls - describe("onInit() enforcedPolicyOptions", () => { - it("should not set enforcedPolicyOptions if there isn't an org invite in deep linked url", async () => { - acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce(null); - // Need to recreate component with new service mock - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - await component.ngOnInit(); - - expect(component.enforcedPolicyOptions).toBe(undefined); - }); - it("should set enforcedPolicyOptions if the deep linked url has an org invite", async () => { - // Set up service method mocks - acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce({ - organizationId: testOrgId, - token: "token", - email: "testEmail", - organizationUserId: "123", - } as OrganizationInvite); - policyApiServiceMock.getPoliciesByToken.mockReturnValueOnce( - Promise.resolve([ - { - id: "345", - organizationId: testOrgId, - type: 1, - data: { - minComplexity: 4, - minLength: 10, - requireLower: null, - requireNumbers: null, - requireSpecial: null, - requireUpper: null, - }, - enabled: true, - }, - ] as Policy[]), - ); - policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue( - of({ - minComplexity: 4, - minLength: 10, - requireLower: null, - requireNumbers: null, - requireSpecial: null, - requireUpper: null, - } as MasterPasswordPolicyOptions), - ); - - // Need to recreate component with new service mocks - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - await component.ngOnInit(); - expect(component.enforcedPolicyOptions).toMatchObject({ - minComplexity: 4, - minLength: 10, - requireLower: null, - requireNumbers: null, - requireSpecial: null, - requireUpper: null, - }); - }); - }); - - // These tests demonstrate route params - describe("Route params", () => { - it("should set org variable to be enterprise and plan to EnterpriseAnnually if org param is enterprise", fakeAsync(() => { - mockQueryParams.next({ org: "enterprise" }); - tick(); // wait for resolution - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - expect(component.org).toBe("enterprise"); - expect(component.plan).toBe(PlanType.EnterpriseAnnually); - })); - it("should not set org variable if no org param is provided", fakeAsync(() => { - mockQueryParams.next({}); - tick(); // wait for resolution - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - expect(component.org).toBe(""); - expect(component.accountCreateOnly).toBe(true); - })); - it("should not set the org if org param is invalid ", fakeAsync(async () => { - mockQueryParams.next({ org: "hahahaha" }); - tick(); // wait for resolution - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - expect(component.org).toBe(""); - expect(component.accountCreateOnly).toBe(true); - })); - it("should set the layout variable if layout param is valid ", fakeAsync(async () => { - mockQueryParams.next({ layout: "teams1" }); - tick(); // wait for resolution - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - expect(component.layout).toBe("teams1"); - expect(component.accountCreateOnly).toBe(false); - })); - it("should not set the layout variable and leave as 'default' if layout param is invalid ", fakeAsync(async () => { - mockQueryParams.next({ layout: "asdfasdf" }); - tick(); // wait for resolution - fixture = TestBed.createComponent(TrialInitiationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - component.ngOnInit(); - expect(component.layout).toBe("default"); - expect(component.accountCreateOnly).toBe(true); - })); - }); - - // These tests demonstrate the use of a stub component - describe("createAccount()", () => { - beforeEach(() => { - component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent) - .componentInstance as VerticalStepperComponent; - }); - - it("should set email and call verticalStepper.next()", fakeAsync(() => { - const verticalStepperNext = jest.spyOn(component.verticalStepper, "next"); - component.createdAccount("test@email.com"); - expect(verticalStepperNext).toHaveBeenCalled(); - expect(component.email).toBe("test@email.com"); - })); - }); - - describe("billingSuccess()", () => { - beforeEach(() => { - component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent) - .componentInstance as VerticalStepperComponent; - }); - - it("should set orgId and call verticalStepper.next()", () => { - const verticalStepperNext = jest.spyOn(component.verticalStepper, "next"); - component.billingSuccess({ orgId: testOrgId }); - expect(verticalStepperNext).toHaveBeenCalled(); - expect(component.orgId).toBe(testOrgId); - }); - }); - - describe("stepSelectionChange()", () => { - beforeEach(() => { - component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent) - .componentInstance as VerticalStepperComponent; - }); - - it("on step 2 should show organization copy text", () => { - component.stepSelectionChange({ - selectedIndex: 1, - previouslySelectedIndex: 0, - } as StepperSelectionEvent); - - expect(component.orgInfoSubLabel).toContain("Enter your"); - expect(component.orgInfoSubLabel).toContain(" organization information"); - }); - it("going from step 2 to 3 should set the orgInforSubLabel to be the Org name from orgInfoFormGroup", () => { - component.orgInfoFormGroup = formBuilder.group({ - name: ["Hooli"], - email: [""], - }); - component.stepSelectionChange({ - selectedIndex: 2, - previouslySelectedIndex: 1, - } as StepperSelectionEvent); - - expect(component.orgInfoSubLabel).toContain("Hooli"); - }); - }); - - describe("previousStep()", () => { - beforeEach(() => { - component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent) - .componentInstance as VerticalStepperComponent; - }); - - it("should call verticalStepper.previous()", fakeAsync(() => { - const verticalStepperPrevious = jest.spyOn(component.verticalStepper, "previous"); - component.previousStep(); - expect(verticalStepperPrevious).toHaveBeenCalled(); - })); - }); - - // These tests demonstrate router navigation - describe("navigation methods", () => { - beforeEach(() => { - component.orgId = testOrgId; - const router = TestBed.inject(Router); - fixture.detectChanges(); - routerSpy = jest.spyOn(router, "navigate"); - }); - describe("navigateToOrgVault", () => { - it("should call verticalStepper.previous()", fakeAsync(() => { - component.navigateToOrgVault(); - expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "vault"]); - })); - }); - describe("navigateToOrgVault", () => { - it("should call verticalStepper.previous()", fakeAsync(() => { - component.navigateToOrgInvite(); - expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "members"]); - })); - }); - }); -}); - -export class VerticalStepperStubComponent extends VerticalStepperComponent {} -export class BlankComponent {} // For router tests diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts deleted file mode 100644 index fbe3eb7aa6d..00000000000 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts +++ /dev/null @@ -1,353 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { StepperSelectionEvent } from "@angular/cdk/stepper"; -import { TitleCasePipe } from "@angular/common"; -import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import { UntypedFormBuilder, Validators } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; - -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.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 { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { - OrganizationInformation, - PlanInformation, - OrganizationBillingServiceAbstraction as OrganizationBillingService, -} from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; - -import { - OrganizationCreatedEvent, - SubscriptionProduct, - TrialOrganizationType, -} from "../../billing/accounts/trial-initiation/trial-billing-step.component"; -import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; -import { OrganizationInvite } from "../organization-invite/organization-invite"; - -import { RouterService } from "./../../core/router.service"; -import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component"; - -export enum ValidOrgParams { - families = "families", - enterprise = "enterprise", - teams = "teams", - teamsStarter = "teamsStarter", - individual = "individual", - premium = "premium", - free = "free", -} - -enum ValidLayoutParams { - default = "default", - teams = "teams", - teams1 = "teams1", - teams2 = "teams2", - teams3 = "teams3", - enterprise = "enterprise", - enterprise1 = "enterprise1", - enterprise2 = "enterprise2", - cnetcmpgnent = "cnetcmpgnent", - cnetcmpgnind = "cnetcmpgnind", - cnetcmpgnteams = "cnetcmpgnteams", - abmenterprise = "abmenterprise", - abmteams = "abmteams", - secretsManager = "secretsManager", -} - -@Component({ - selector: "app-trial", - templateUrl: "trial-initiation.component.html", -}) -export class TrialInitiationComponent implements OnInit, OnDestroy { - email = ""; - fromOrgInvite = false; - org = ""; - orgInfoSubLabel = ""; - orgId = ""; - orgLabel = ""; - billingSubLabel = ""; - layout = "default"; - plan: PlanType; - productTier: ProductTierType; - accountCreateOnly = true; - useTrialStepper = false; - loading = false; - policies: Policy[]; - enforcedPolicyOptions: MasterPasswordPolicyOptions; - trialFlowOrgs: string[] = [ - ValidOrgParams.teams, - ValidOrgParams.teamsStarter, - ValidOrgParams.enterprise, - ValidOrgParams.families, - ]; - routeFlowOrgs: string[] = [ - ValidOrgParams.free, - ValidOrgParams.premium, - ValidOrgParams.individual, - ]; - layouts = ValidLayoutParams; - referenceData: ReferenceEventRequest; - @ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent; - - orgInfoFormGroup = this.formBuilder.group({ - name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }], - email: [""], - }); - - private set referenceDataId(referenceId: string) { - if (referenceId != null) { - this.referenceData.id = referenceId; - } else { - this.referenceData.id = ("; " + document.cookie) - .split("; reference=") - .pop() - .split(";") - .shift(); - } - - if (this.referenceData.id === "") { - this.referenceData.id = null; - } else { - // Matches "_ga_QBRN562QQQ=value1.value2.session" and captures values and session. - const regex = /_ga_QBRN562QQQ=([^.]+)\.([^.]+)\.(\d+)/; - const match = document.cookie.match(regex); - if (match) { - this.referenceData.session = match[3]; - } - } - } - - private destroy$ = new Subject(); - protected enableTrialPayment$ = this.configService.getFeatureFlag$( - FeatureFlag.TrialPaymentOptional, - ); - - constructor( - private route: ActivatedRoute, - protected router: Router, - private formBuilder: UntypedFormBuilder, - private titleCasePipe: TitleCasePipe, - private logService: LogService, - private policyApiService: PolicyApiServiceAbstraction, - private policyService: PolicyService, - private i18nService: I18nService, - private routerService: RouterService, - private acceptOrgInviteService: AcceptOrganizationInviteService, - private organizationBillingService: OrganizationBillingService, - private configService: ConfigService, - ) {} - - async ngOnInit(): Promise { - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => { - this.referenceData = new ReferenceEventRequest(); - if (qParams.email != null && qParams.email.indexOf("@") > -1) { - this.email = qParams.email; - this.fromOrgInvite = qParams.fromOrgInvite === "true"; - } - - this.referenceDataId = qParams.reference; - - if (Object.values(ValidLayoutParams).includes(qParams.layout)) { - this.layout = qParams.layout; - this.accountCreateOnly = false; - } - - if (this.trialFlowOrgs.includes(qParams.org)) { - this.org = qParams.org; - this.orgLabel = this.titleCasePipe.transform(this.orgDisplayName); - this.useTrialStepper = true; - this.referenceData.flow = qParams.org; - - if (this.org === ValidOrgParams.families) { - this.plan = PlanType.FamiliesAnnually; - this.productTier = ProductTierType.Families; - } else if (this.org === ValidOrgParams.teamsStarter) { - this.plan = PlanType.TeamsStarter; - this.productTier = ProductTierType.TeamsStarter; - } else if (this.org === ValidOrgParams.teams) { - this.plan = PlanType.TeamsAnnually; - this.productTier = ProductTierType.Teams; - } else if (this.org === ValidOrgParams.enterprise) { - this.plan = PlanType.EnterpriseAnnually; - this.productTier = ProductTierType.Enterprise; - } - } else if (this.routeFlowOrgs.includes(qParams.org)) { - this.referenceData.flow = qParams.org; - const route = this.router.createUrlTree(["create-organization"], { - queryParams: { plan: qParams.org }, - }); - this.routerService.setPreviousUrl(route.toString()); - } - - // Are they coming from an email for sponsoring a families organization - // After logging in redirect them to setup the families sponsorship - this.setupFamilySponsorship(qParams.sponsorshipToken); - - this.referenceData.initiationPath = this.accountCreateOnly - ? "Registration form" - : "Password Manager trial from marketing website"; - }); - - // If there's a deep linked org invite, use it to get the password policies - const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); - if (orgInvite != null) { - await this.initPasswordPolicies(orgInvite); - } - - this.orgInfoFormGroup.controls.name.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.orgInfoFormGroup.controls.name.markAsTouched(); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - stepSelectionChange(event: StepperSelectionEvent) { - // Set org info sub label - if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") { - this.orgInfoSubLabel = - "Enter your " + - this.titleCasePipe.transform(this.orgDisplayName) + - " organization information"; - } else if (event.previouslySelectedIndex === 1) { - this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value; - } - - //set billing sub label - if (event.selectedIndex === 2) { - this.billingSubLabel = this.i18nService.t("billingTrialSubLabel"); - } - } - - async createOrganizationOnTrial() { - this.loading = true; - const organization: OrganizationInformation = { - name: this.orgInfoFormGroup.get("name").value, - billingEmail: this.orgInfoFormGroup.get("email").value, - initiationPath: "Password Manager trial from marketing website", - }; - - const plan: PlanInformation = { - type: this.plan, - passwordManagerSeats: 1, - }; - - const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ - organization, - plan, - }); - - this.orgId = response?.id; - this.billingSubLabel = `${this.i18nService.t("annual")} ($0/${this.i18nService.t("yr")})`; - this.loading = false; - this.verticalStepper.next(); - } - - createdAccount(email: string) { - this.email = email; - this.orgInfoFormGroup.get("email")?.setValue(email); - this.verticalStepper.next(); - } - - billingSuccess(event: any) { - this.orgId = event?.orgId; - this.billingSubLabel = event?.subLabelText; - this.verticalStepper.next(); - } - - createdOrganization(event: OrganizationCreatedEvent) { - this.orgId = event.organizationId; - this.billingSubLabel = event.planDescription; - this.verticalStepper.next(); - } - - navigateToOrgVault() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["organizations", this.orgId, "vault"]); - } - - navigateToOrgInvite() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["organizations", this.orgId, "members"]); - } - - previousStep() { - this.verticalStepper.previous(); - } - - get orgDisplayName() { - if (this.org === "teamsStarter") { - return "Teams Starter"; - } - - return this.org; - } - - get freeTrialText() { - const translationKey = - this.layout === this.layouts.secretsManager - ? "startYour7DayFreeTrialOfBitwardenSecretsManagerFor" - : "startYour7DayFreeTrialOfBitwardenFor"; - - return this.i18nService.t(translationKey, this.org); - } - - get trialOrganizationType(): TrialOrganizationType { - switch (this.productTier) { - case ProductTierType.Free: - return null; - default: - return this.productTier; - } - } - - private setupFamilySponsorship(sponsorshipToken: string) { - if (sponsorshipToken != null) { - const route = this.router.createUrlTree(["setup/families-for-enterprise"], { - queryParams: { plan: sponsorshipToken }, - }); - this.routerService.setPreviousUrl(route.toString()); - } - } - - private async initPasswordPolicies(invite: OrganizationInvite): Promise { - if (invite == null) { - return; - } - - try { - this.policies = await this.policyApiService.getPoliciesByToken( - invite.organizationId, - invite.token, - invite.email, - invite.organizationUserId, - ); - } catch (e) { - this.logService.error(e); - } - - if (this.policies != null) { - this.policyService - .masterPasswordPolicyOptions$(this.policies) - .pipe(takeUntil(this.destroy$)) - .subscribe((enforcedPasswordPolicyOptions) => { - this.enforcedPolicyOptions = enforcedPasswordPolicyOptions; - }); - } - } - - protected readonly SubscriptionProduct = SubscriptionProduct; -} diff --git a/apps/web/src/app/auth/two-factor-auth-duo.component.ts b/apps/web/src/app/auth/two-factor-auth-duo.component.ts index b82632008bd..971c1f3764c 100644 --- a/apps/web/src/app/auth/two-factor-auth-duo.component.ts +++ b/apps/web/src/app/auth/two-factor-auth-duo.component.ts @@ -8,11 +8,23 @@ import { ReactiveFormsModule, FormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthDuoComponent as TwoFactorAuthDuoBaseComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LinkModule } from "../../../../../libs/components/src/link"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; @Component({ diff --git a/apps/web/src/app/auth/two-factor-auth.component.ts b/apps/web/src/app/auth/two-factor-auth.component.ts index 18660b2ca63..cbe1d8f0a53 100644 --- a/apps/web/src/app/auth/two-factor-auth.component.ts +++ b/apps/web/src/app/auth/two-factor-auth.component.ts @@ -25,19 +25,39 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthAuthenticatorComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthEmailComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthWebAuthnComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthYubikeyComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TwoFactorOptionsComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "../../../../../libs/auth/src/common/abstractions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html index 2f983944b70..361b2d9a3a3 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html @@ -19,7 +19,7 @@
-

+

{{ "premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount }} @@ -49,49 +49,58 @@ linkType="primary" routerLink="/create-organization" [queryParams]="{ plan: 'families' }" - >{{ "bitwardenFamiliesPlan" | i18n }} + {{ "bitwardenFamiliesPlan" | i18n }} +

{{ "purchasePremium" | i18n }} - -

{{ "uploadLicenseFilePremium" | i18n }}

-
- - {{ "licenseFile" | i18n }} -
- - {{ this.licenseFile ? this.licenseFile.name : ("noFileChosen" | i18n) }} -
- - {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} -
- -
+ + +

{{ "uploadLicenseFilePremium" | i18n }}

+
+ + {{ "licenseFile" | i18n }} +
+ + {{ + licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n) + }} +
+ + {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} +
+ +
+
+
-
+

{{ "addons" | i18n }}

@@ -106,7 +115,7 @@ /> {{ "additionalStorageIntervalDesc" - | i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n) + | i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n) }}
@@ -114,30 +123,26 @@

{{ "summary" | i18n }}

{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
- {{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = - {{ additionalStorageTotal | currency: "$" }} + {{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB × + {{ storageGBPrice | currency: "$" }} = + {{ additionalStorageCost | currency: "$" }}

{{ "paymentInformation" | i18n }}

- - -
-
- {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} -
- - {{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }} - + + +
+
+ {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} + {{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}
-
-

- {{ "total" | i18n }}: {{ total | currency: "USD $" }}/{{ "year" | i18n }} -

-

{{ "paymentChargedAnnually" | i18n }}

- diff --git a/apps/web/src/app/billing/individual/premium/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts index f96f573cd4d..ec19eb02594 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -1,187 +1,197 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnInit, ViewChild } from "@angular/core"; +import { Component, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; -import { firstValueFrom, Observable, switchMap } from "rxjs"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, concatMap, from, Observable, of, switchMap } from "rxjs"; import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { ToastService } from "@bitwarden/components"; -import { PaymentComponent, TaxInfoComponent } from "../../shared"; +import { PaymentComponent } from "../../shared/payment/payment.component"; +import { TaxInfoComponent } from "../../shared/tax-info.component"; @Component({ - templateUrl: "premium.component.html", + templateUrl: "./premium.component.html", }) -export class PremiumComponent implements OnInit { +export class PremiumComponent { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; - canAccessPremium$: Observable; - selfHosted = false; - premiumPrice = 10; - familyPlanMaxUserCount = 6; - storageGbPrice = 4; - cloudWebVaultUrl: string; - licenseFile: File = null; + protected hasPremiumFromAnyOrganization$: Observable; - formPromise: Promise; - protected licenseForm = new FormGroup({ - file: new FormControl(null, [Validators.required]), - }); - protected addonForm = new FormGroup({ - additionalStorage: new FormControl(0, [Validators.max(99), Validators.min(0)]), + protected addOnFormGroup = new FormGroup({ + additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]), }); - private estimatedTax: number = 0; + protected licenseFormGroup = new FormGroup({ + file: new FormControl(null, [Validators.required]), + }); + + protected cloudWebVaultURL: string; + protected isSelfHost = false; + + protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$( + FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader, + ); + + protected estimatedTax: number = 0; + protected readonly familyPlanMaxUserCount = 6; + protected readonly premiumPrice = 10; + protected readonly storageGBPrice = 4; constructor( + private activatedRoute: ActivatedRoute, private apiService: ApiService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private configService: ConfigService, + private environmentService: EnvironmentService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private tokenService: TokenService, private router: Router, - private messagingService: MessagingService, private syncService: SyncService, - private environmentService: EnvironmentService, - private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, + private tokenService: TokenService, private taxService: TaxServiceAbstraction, private accountService: AccountService, ) { - this.selfHosted = platformUtilsService.isSelfHost(); - this.canAccessPremium$ = this.accountService.activeAccount$.pipe( + this.isSelfHost = this.platformUtilsService.isSelfHost(); + + this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe( switchMap((account) => - this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id), ), ); - this.addonForm.controls.additionalStorage.valueChanges + combineLatest([ + this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumPersonally$(account.id), + ), + ), + this.environmentService.cloudWebVaultUrl$, + ]) + .pipe( + takeUntilDestroyed(), + concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => { + if (hasPremiumPersonally) { + return from(this.navigateToSubscriptionPage()); + } + + this.cloudWebVaultURL = cloudWebVaultURL; + return of(true); + }), + ) + .subscribe(); + + this.addOnFormGroup.controls.additionalStorage.valueChanges .pipe(debounceTime(1000), takeUntilDestroyed()) .subscribe(() => { this.refreshSalesTax(); }); } - protected setSelectedFile(event: Event) { - const fileInputEl = event.target; - const file: File = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null; - this.licenseFile = file; - } - async ngOnInit() { - this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); - const account = await firstValueFrom(this.accountService.activeAccount$); - if ( - await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)) - ) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/settings/subscription/user-subscription"]); - return; - } - } - submit = async () => { - if (this.taxInfoComponent) { - if (!this.taxInfoComponent?.taxFormGroup.valid) { - this.taxInfoComponent.taxFormGroup.markAllAsTouched(); - return; - } - } - this.licenseForm.markAllAsTouched(); - this.addonForm.markAllAsTouched(); - if (this.selfHosted) { - if (this.licenseFile == null) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("selectFile"), - }); - return; - } - } - if (this.selfHosted) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - if (!this.tokenService.getEmailVerified()) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("verifyEmailFirst"), - }); - return; - } - - const fd = new FormData(); - fd.append("license", this.licenseFile); - await this.apiService.postAccountLicense(fd).then(() => { - return this.finalizePremium(); - }); - } else { - await this.paymentComponent - .createPaymentToken() - .then((result) => { - const fd = new FormData(); - fd.append("paymentMethodType", result[1].toString()); - if (result[0] != null) { - fd.append("paymentToken", result[0]); - } - fd.append("additionalStorageGb", (this.additionalStorage || 0).toString()); - fd.append("country", this.taxInfoComponent?.taxFormGroup?.value.country); - fd.append("postalCode", this.taxInfoComponent?.taxFormGroup?.value.postalCode); - return this.apiService.postPremium(fd); - }) - .then((paymentResponse) => { - if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) { - return this.paymentComponent.handleStripeCardPayment( - paymentResponse.paymentIntentClientSecret, - () => this.finalizePremium(), - ); - } else { - return this.finalizePremium(); - } - }); - } - }; - - async finalizePremium() { + finalizeUpgrade = async () => { await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); + }; + + postFinalizeUpgrade = async () => { this.toastService.showToast({ variant: "success", title: null, message: this.i18nService.t("premiumUpdated"), }); - await this.router.navigate(["/settings/subscription/user-subscription"]); + await this.navigateToSubscriptionPage(); + }; + + navigateToSubscriptionPage = (): Promise => + this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); + + onLicenseFileSelected = (event: Event): void => { + const element = event.target as HTMLInputElement; + this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null; + }; + + submitPremiumLicense = async (): Promise => { + this.licenseFormGroup.markAllAsTouched(); + + if (this.licenseFormGroup.invalid) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); + } + + const emailVerified = await this.tokenService.getEmailVerified(); + if (!emailVerified) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verifyEmailFirst"), + }); + } + + const formData = new FormData(); + formData.append("license", this.licenseFormGroup.value.file); + + await this.apiService.postAccountLicense(formData); + await this.finalizeUpgrade(); + await this.postFinalizeUpgrade(); + }; + + submitPayment = async (): Promise => { + this.taxInfoComponent.taxFormGroup.markAllAsTouched(); + if (this.taxInfoComponent.taxFormGroup.invalid) { + return; + } + + const { type, token } = await this.paymentComponent.tokenize(); + + const formData = new FormData(); + formData.append("paymentMethodType", type.toString()); + formData.append("paymentToken", token); + formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString()); + formData.append("country", this.taxInfoComponent.country); + formData.append("postalCode", this.taxInfoComponent.postalCode); + + await this.apiService.postPremium(formData); + await this.finalizeUpgrade(); + await this.postFinalizeUpgrade(); + }; + + protected get additionalStorageCost(): number { + return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage; } - get additionalStorage(): number { - return this.addonForm.get("additionalStorage").value; - } - get additionalStorageTotal(): number { - return this.storageGbPrice * Math.abs(this.additionalStorage || 0); + protected get premiumURL(): string { + return `${this.cloudWebVaultURL}/#/settings/subscription/premium`; } - get subtotal(): number { - return this.premiumPrice + this.additionalStorageTotal; + protected get subtotal(): number { + return this.premiumPrice + this.additionalStorageCost; } - get taxCharges(): number { - return this.estimatedTax; + protected get total(): number { + return this.subtotal + this.estimatedTax; } - get total(): number { - return this.subtotal + this.taxCharges || 0; + protected async onLicenseFileSelectedChanged(): Promise { + await this.postFinalizeUpgrade(); } private refreshSalesTax(): void { @@ -190,7 +200,7 @@ export class PremiumComponent implements OnInit { } const request: PreviewIndividualInvoiceRequest = { passwordManager: { - additionalStorage: this.addonForm.value.additionalStorage, + additionalStorage: this.addOnFormGroup.value.additionalStorage, }, taxInformation: { postalCode: this.taxInfoComponent.postalCode, diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 97b4725e6d7..38f4436fb47 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -8,8 +8,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -18,12 +16,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { DialogService, ToastService } from "@bitwarden/components"; import { - AdjustStorageDialogV2Component, - AdjustStorageDialogV2ResultType, -} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; -import { - AdjustStorageDialogResult, - openAdjustStorageDialog, + AdjustStorageDialogComponent, + AdjustStorageDialogResultType, } from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, @@ -45,10 +39,6 @@ export class UserSubscriptionComponent implements OnInit { cancelPromise: Promise; reinstatePromise: Promise; - protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -60,7 +50,6 @@ export class UserSubscriptionComponent implements OnInit { private environmentService: EnvironmentService, private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, - private configService: ConfigService, private accountService: AccountService, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); @@ -166,33 +155,18 @@ export class UserSubscriptionComponent implements OnInit { }; adjustStorage = async (add: boolean) => { - const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); + const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, { + data: { + price: 4, + cadence: "year", + type: add ? "Add" : "Remove", + }, + }); - if (deprecateStripeSourcesAPI) { - const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { - data: { - price: 4, - cadence: "year", - type: add ? "Add" : "Remove", - }, - }); + const result = await lastValueFrom(dialogRef.closed); - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustStorageDialogV2ResultType.Submitted) { - await this.load(); - } - } else { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: 4, - add: add, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); - } + if (result === AdjustStorageDialogResultType.Submitted) { + await this.load(); } }; diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index 0166560007e..786c25c8d4c 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -2,11 +2,16 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + InternalOrganizationServiceAbstraction, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService } from "@bitwarden/components"; @@ -37,6 +42,7 @@ export class AdjustSubscription implements OnInit, OnDestroy { private formBuilder: FormBuilder, private toastService: ToastService, private internalOrganizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { @@ -73,14 +79,19 @@ export class AdjustSubscription implements OnInit, OnDestroy { request, ); - const organization = await this.internalOrganizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organization = await firstValueFrom( + this.internalOrganizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const organizationData = new OrganizationData(response, { isMember: organization.isMember, isProviderUser: organization.isProviderUser, }); - await this.internalOrganizationService.upsert(organizationData); + await this.internalOrganizationService.upsert(organizationData, userId); this.toastService.showToast({ variant: "success", diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index 96679ea1753..ca1b9245c0b 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -118,7 +118,13 @@ ) | currency: "$" }} - /{{ "monthPerMember" | i18n }} + + /{{ + selectableProduct.productTier === productTypes.Families + ? "month" + : ("monthPerMember" | i18n) + }} - {{ - deprecateStripeSourcesAPI - ? paymentSource?.description - : billing?.paymentSource?.description - }} + {{ paymentSource?.description }} {{ "changePaymentMethod" | i18n }}

- - + -
+

{{ "total" | i18n }}: @@ -962,7 +963,7 @@

-
+

; isSubscriptionCanceled: boolean = false; private destroy$ = new Subject(); @@ -205,17 +203,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private messagingService: MessagingService, private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, private taxService: TaxServiceAbstraction, + private accountService: AccountService, private organizationBillingService: OrganizationBillingService, ) {} async ngOnInit(): Promise { - this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - if (this.dialogParams.organizationId) { this.currentPlanName = this.resolvePlanName(this.dialogParams.productTierType); this.sub = @@ -225,21 +219,24 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.organizationId = this.dialogParams.organizationId; this.currentPlan = this.sub?.plan; this.selectedPlan = this.sub?.plan; - this.organization = await this.organizationService.get(this.organizationId); - if (this.deprecateStripeSourcesAPI) { - const { accountCredit, paymentSource } = - await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); - this.accountCredit = accountCredit; - this.paymentSource = paymentSource; - } else { - this.billing = await this.organizationApiService.getBilling(this.organizationId); - } + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); + const { accountCredit, paymentSource } = + await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); + this.accountCredit = accountCredit; + this.paymentSource = paymentSource; } if (!this.selfHosted) { - const plans = await this.apiService.getPlans(); - this.passwordManagerPlans = plans.data.filter((plan) => !!plan.PasswordManager); - this.secretsManagerPlans = plans.data.filter((plan) => !!plan.SecretsManager); + this.plans = await this.apiService.getPlans(); + this.passwordManagerPlans = this.plans.data.filter((plan) => !!plan.PasswordManager); + this.secretsManagerPlans = this.plans.data.filter((plan) => !!plan.SecretsManager); if ( this.productTier === ProductTierType.Enterprise || @@ -320,16 +317,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return this.selectableProducts.find((product) => product.productTier === productTier); } - secretsManagerTrialDiscount() { - return this.sub?.customerDiscount?.appliesTo?.includes("sm-standalone") - ? this.discountPercentage - : this.discountPercentageFromSub + this.discountPercentage; - } - isPaymentSourceEmpty() { - return this.deprecateStripeSourcesAPI - ? this.paymentSource === null || this.paymentSource === undefined - : this.billing?.paymentSource === null || this.billing?.paymentSource === undefined; + return this.paymentSource === null || this.paymentSource === undefined; } isSecretsManagerTrial(): boolean { @@ -473,9 +462,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { get upgradeRequiresPaymentMethod() { const isFreeTier = this.organization?.productTierType === ProductTierType.Free; const shouldHideFree = !this.showFree; - const hasNoPaymentSource = this.deprecateStripeSourcesAPI - ? !this.paymentSource - : !this.billing?.paymentSource; + const hasNoPaymentSource = !this.paymentSource; return isFreeTier && shouldHideFree && hasNoPaymentSource; } @@ -708,25 +695,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } changedCountry() { - if (this.deprecateStripeSourcesAPI && this.paymentV2Component) { - this.paymentV2Component.showBankAccount = this.taxInformation.country === "US"; + this.paymentComponent.showBankAccount = this.taxInformation.country === "US"; - if ( - !this.paymentV2Component.showBankAccount && - this.paymentV2Component.selected === PaymentMethodType.BankAccount - ) { - this.paymentV2Component.select(PaymentMethodType.Card); - } - } else if (this.paymentComponent && this.taxInformation) { - this.paymentComponent!.hideBank = this.taxInformation.country !== "US"; - // Bank Account payments are only available for US customers - if ( - this.paymentComponent.hideBank && - this.paymentComponent.method === PaymentMethodType.BankAccount - ) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); } } @@ -791,8 +766,15 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { billingEmail: org.billingEmail, }; + const filteredPlan = this.plans.data + .filter((plan) => plan.productTier === this.selectedPlan.productTier && !plan.legacyYear) + .find((plan) => { + const isSameBillingCycle = plan.isAnnual === this.selectedPlan.isAnnual; + return isSameBillingCycle; + }); + const plan: PlanInformation = { - type: this.selectedPlan.type, + type: filteredPlan.type, passwordManagerSeats: org.seats, }; @@ -801,14 +783,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { plan.secretsManagerSeats = org.smSeats; } - let paymentMethod: [string, PaymentMethodType]; - - if (this.deprecateStripeSourcesAPI) { - const { type, token } = await this.paymentV2Component.tokenize(); - paymentMethod = [token, type]; - } else { - paymentMethod = await this.paymentComponent.createPaymentToken(); - } + const { type, token } = await this.paymentComponent.tokenize(); + const paymentMethod: [string, PaymentMethodType] = [token, type]; const payment: PaymentInformation = { paymentMethod, @@ -844,27 +820,17 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) { - if (this.deprecateStripeSourcesAPI) { - const tokenizedPaymentSource = await this.paymentV2Component.tokenize(); - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, - ); + const tokenizedPaymentSource = await this.paymentComponent.tokenize(); + const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); + updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; + updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( + this.taxInformation, + ); - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, - ); - } else { - const tokenResult = await this.paymentComponent.createPaymentToken(); - const paymentRequest = new PaymentRequest(); - paymentRequest.paymentToken = tokenResult[0]; - paymentRequest.paymentMethodType = tokenResult[1]; - paymentRequest.country = this.taxInformation.country; - paymentRequest.postalCode = this.taxInformation.postalCode; - await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); - } + await this.billingApiService.updateOrganizationPaymentMethod( + this.organizationId, + updatePaymentMethodRequest, + ); } // Backfill pub/priv key if necessary @@ -874,10 +840,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } - const result = await this.organizationApiService.upgrade(this.organizationId, request); - if (!result.success && result.paymentIntentClientSecret != null) { - await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null); - } + await this.organizationApiService.upgrade(this.organizationId, request); return this.organizationId; } @@ -974,38 +937,20 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get paymentSourceClasses() { - if (this.deprecateStripeSourcesAPI) { - if (this.paymentSource == null) { + if (this.paymentSource == null) { + return []; + } + switch (this.paymentSource.type) { + case PaymentMethodType.Card: + return ["bwi-credit-card"]; + case PaymentMethodType.BankAccount: + return ["bwi-bank"]; + case PaymentMethodType.Check: + return ["bwi-money"]; + case PaymentMethodType.PayPal: + return ["bwi-paypal text-primary"]; + default: return []; - } - switch (this.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - return ["bwi-bank"]; - case PaymentMethodType.Check: - return ["bwi-money"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } - } else { - if (this.billing.paymentSource == null) { - return []; - } - switch (this.billing.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - return ["bwi-bank"]; - case PaymentMethodType.Check: - return ["bwi-money"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } } } @@ -1100,10 +1045,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.estimatedTax = invoice.taxAmount; }) .catch((error) => { + const translatedMessage = this.i18nService.t(error.message); this.toastService.showToast({ title: "", variant: "error", - message: this.i18nService.t(error.message), + message: + !translatedMessage || translatedMessage === "" ? error.message : translatedMessage, }); }); } diff --git a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts index 3d4c8dd3870..1bfb9fc4912 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts @@ -1,14 +1,11 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { canAccessBillingTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard"; import { organizationIsUnmanaged } from "../../billing/guards/organization-is-unmanaged.guard"; import { WebPlatformUtilsService } from "../../core/web-platform-utils.service"; -import { PaymentMethodComponent } from "../shared"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; @@ -28,21 +25,17 @@ const routes: Routes = [ : OrganizationSubscriptionCloudComponent, data: { titleId: "subscription" }, }, - ...featureFlaggedRoute({ - defaultComponent: PaymentMethodComponent, - flaggedComponent: OrganizationPaymentMethodComponent, - featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - routeOptions: { - path: "payment-method", - canActivate: [ - organizationPermissionsGuard((org) => org.canEditPaymentMethods), - organizationIsUnmanaged, - ], - data: { - titleId: "paymentMethod", - }, + { + path: "payment-method", + component: OrganizationPaymentMethodComponent, + canActivate: [ + organizationPermissionsGuard((org) => org.canEditPaymentMethods), + organizationIsUnmanaged, + ], + data: { + titleId: "paymentMethod", }, - }), + }, { path: "history", component: OrgBillingHistoryViewComponent, diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index 48ac613711d..d8f4b7393aa 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -1,5 +1,7 @@ import { NgModule } from "@angular/core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BannerModule } from "../../../../../../libs/components/src/banner/banner.module"; import { UserVerificationModule } from "../../auth/shared/components/user-verification"; import { LooseComponentsModule } from "../../shared"; diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index d37f95e3aa2..0a4eea57f92 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -434,12 +434,10 @@ {{ paymentDesc }}

- + *ngIf="createOrganization || upgradeRequiresPaymentMethod" + [showAccountCredit]="false" + > +
- - - diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 94e8637e8a3..071d1f75161 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -11,13 +11,16 @@ import { } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; -import { debounceTime } from "rxjs/operators"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { debounceTime, map } from "rxjs/operators"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -27,20 +30,20 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/ import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-organization-create.request"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { TaxInformation } from "@bitwarden/common/billing/models/domain"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -53,7 +56,6 @@ import { KeyService } from "@bitwarden/key-management"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared"; -import { PaymentV2Component } from "../shared/payment/payment-v2.component"; import { PaymentComponent } from "../shared/payment/payment.component"; interface OnSuccessArgs { @@ -75,7 +77,6 @@ const Allowed2020PlansForLegacyProviders = [ }) export class OrganizationPlansComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component; @ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent; @Input() organizationId?: string; @@ -124,7 +125,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { singleOrgPolicyAppliesToActiveUser = false; isInTrialFlow = false; discount = 0; - deprecateStripeSourcesAPI: boolean; protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$( FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader, @@ -179,17 +179,21 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, private taxService: TaxServiceAbstraction, + private accountService: AccountService, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); } async ngOnInit() { - this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - if (this.organizationId) { - this.organization = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); this.billing = await this.organizationApiService.getBilling(this.organizationId); this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.taxInformation = await this.organizationApiService.getTaxInfo(this.organizationId); @@ -568,23 +572,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } protected changedCountry(): void { - if (this.deprecateStripeSourcesAPI) { - this.paymentV2Component.showBankAccount = this.taxInformation?.country === "US"; - if ( - !this.paymentV2Component.showBankAccount && - this.paymentV2Component.selected === PaymentMethodType.BankAccount - ) { - this.paymentV2Component.select(PaymentMethodType.Card); - } - } else { - this.paymentComponent.hideBank = this.taxInformation?.country !== "US"; - if ( - this.paymentComponent.hideBank && - this.paymentComponent.method === PaymentMethodType.BankAccount - ) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } + this.paymentComponent.showBankAccount = this.taxInformation?.country === "US"; + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); } } @@ -739,25 +732,15 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod) { - if (this.deprecateStripeSourcesAPI) { - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = await this.paymentV2Component.tokenize(); - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, - ); - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, - ); - } else { - const [paymentToken, paymentMethodType] = await this.paymentComponent.createPaymentToken(); - const paymentRequest = new PaymentRequest(); - paymentRequest.paymentToken = paymentToken; - paymentRequest.paymentMethodType = paymentMethodType; - paymentRequest.country = this.taxInformation?.country; - paymentRequest.postalCode = this.taxInformation?.postalCode; - await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); - } + const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); + updatePaymentMethodRequest.paymentSource = await this.paymentComponent.tokenize(); + updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( + this.taxInformation, + ); + await this.billingApiService.updateOrganizationPaymentMethod( + this.organizationId, + updatePaymentMethodRequest, + ); } // Backfill pub/priv key if necessary @@ -767,10 +750,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } - const result = await this.organizationApiService.upgrade(this.organizationId, request); - if (!result.success && result.paymentIntentClientSecret != null) { - await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null); - } + await this.organizationApiService.upgrade(this.organizationId, request); return this.organizationId; } @@ -791,14 +771,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.selectedPlan.type === PlanType.Free) { request.planType = PlanType.Free; } else { - let type: PaymentMethodType; - let token: string; - - if (this.deprecateStripeSourcesAPI) { - ({ type, token } = await this.paymentV2Component.tokenize()); - } else { - [token, type] = await this.paymentComponent.createPaymentToken(); - } + const { type, token } = await this.paymentComponent.tokenize(); request.paymentToken = token; request.paymentMethodType = type; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 0cd21d0f688..1d8a7846d9d 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -294,6 +294,10 @@ + +

{{ "manageSubscription" | i18n }}

+

{{ resellerSeatsRemainingMessage }}

+

{{ "selfHostingTitleProper" | i18n }} diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 0805e92ee2a..50c755af63b 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -4,11 +4,20 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { firstValueFrom, lastValueFrom, Observable, Subject } from "rxjs"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + OrganizationApiKeyType, + OrganizationUserStatusType, +} from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -20,12 +29,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { DialogService, ToastService } from "@bitwarden/components"; import { - AdjustStorageDialogV2Component, - AdjustStorageDialogV2ResultType, -} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; -import { - AdjustStorageDialogResult, - openAdjustStorageDialog, + AdjustStorageDialogComponent, + AdjustStorageDialogResultType, } from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, @@ -50,7 +55,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy organizationId: string; userOrg: Organization; showChangePlan = false; - showDownloadLicense = false; hasBillingSyncToken: boolean; showAdjustSecretsManager = false; showSecretsManagerSubscribe = false; @@ -61,27 +65,28 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy showSubscription = true; showSelfHost = false; organizationIsManagedByConsolidatedBillingMSP = false; + resellerSeatsRemainingMessage: string; protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon; protected readonly teamsStarter = ProductTierType.TeamsStarter; - protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - private destroy$ = new Subject(); + private seatsRemainingMessage: string; + constructor( private apiService: ApiService, private i18nService: I18nService, private logService: LogService, private organizationService: OrganizationService, + private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private route: ActivatedRoute, private dialogService: DialogService, private configService: ConfigService, private toastService: ToastService, private billingApiService: BillingApiServiceAbstraction, + private organizationUserApiService: OrganizationUserApiService, ) {} async ngOnInit() { @@ -107,6 +112,28 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } } } + + if (this.userOrg.hasReseller) { + const allUsers = await this.organizationUserApiService.getAllUsers(this.userOrg.id); + + const userCount = allUsers.data.filter((user) => + [ + OrganizationUserStatusType.Invited, + OrganizationUserStatusType.Accepted, + OrganizationUserStatusType.Confirmed, + ].includes(user.status), + ).length; + + const remainingSeats = this.userOrg.seats - userCount; + + const seatsRemaining = this.i18nService.t( + "seatsRemaining", + remainingSeats.toString(), + this.userOrg.seats.toString(), + ); + + this.resellerSeatsRemainingMessage = seatsRemaining; + } } ngOnDestroy() { @@ -117,7 +144,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy async load() { this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); - this.userOrg = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.userOrg = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const isIndependentOrganizationOwner = !this.userOrg.hasProvider && this.userOrg.isOwner; const isResoldOrganizationOwner = this.userOrg.hasReseller && this.userOrg.isOwner; @@ -415,36 +447,19 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy adjustStorage = (add: boolean) => { return async () => { - const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); + const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, { + data: { + price: this.storageGbPrice, + cadence: this.billingInterval, + type: add ? "Add" : "Remove", + organizationId: this.organizationId, + }, + }); - if (deprecateStripeSourcesAPI) { - const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { - data: { - price: this.storageGbPrice, - cadence: this.billingInterval, - type: add ? "Add" : "Remove", - organizationId: this.organizationId, - }, - }); + const result = await lastValueFrom(dialogRef.closed); - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustStorageDialogV2ResultType.Submitted) { - await this.load(); - } - } else { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: this.storageGbPrice, - add: add, - organizationId: this.organizationId, - interval: this.billingInterval, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); - } + if (result === AdjustStorageDialogResultType.Submitted) { + await this.load(); } }; }; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts index ef68de39526..e6854a5216b 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts @@ -7,10 +7,15 @@ import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationConnectionType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationConnectionResponse } from "@bitwarden/common/admin-console/models/response/organization-connection.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { BillingSyncConfigApi } from "@bitwarden/common/billing/models/api/billing-sync-config.api"; import { SelfHostedOrganizationSubscriptionView } from "@bitwarden/common/billing/models/view/self-hosted-organization-subscription.view"; @@ -80,6 +85,7 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest private messagingService: MessagingService, private apiService: ApiService, private organizationService: OrganizationService, + private accountService: AccountService, private route: ActivatedRoute, private organizationApiService: OrganizationApiServiceAbstraction, private platformUtilsService: PlatformUtilsService, @@ -115,7 +121,12 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest return; } this.loading = true; - this.userOrg = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.userOrg = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); this.showAutomaticSyncAndManualUpload = this.userOrg.productTierType == ProductTierType.Families ? false : true; if (this.userOrg.canViewSubscription) { diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index 270ba54f70d..a8b2c7a46f1 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -4,11 +4,15 @@ import { Location } from "@angular/common"; import { Component, OnDestroy } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; -import { from, lastValueFrom, switchMap } from "rxjs"; +import { firstValueFrom, from, lastValueFrom, map, switchMap } from "rxjs"; 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 { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; @@ -19,16 +23,16 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; -import { FreeTrial } from "../../../core/types/free-trial"; import { TrialFlowService } from "../../services/trial-flow.service"; import { AddCreditDialogResult, openAddCreditDialog, } from "../../shared/add-credit-dialog.component"; import { - AdjustPaymentDialogV2Component, - AdjustPaymentDialogV2ResultType, -} from "../../shared/adjust-payment-dialog/adjust-payment-dialog-v2.component"; + AdjustPaymentDialogComponent, + AdjustPaymentDialogResultType, +} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component"; +import { FreeTrial } from "../../types/free-trial"; @Component({ templateUrl: "./organization-payment-method.component.html", @@ -60,6 +64,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { private location: Location, private trialFlowService: TrialFlowService, private organizationService: OrganizationService, + private accountService: AccountService, protected syncService: SyncService, ) { this.activatedRoute.params @@ -120,7 +125,14 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); - const organizationPromise = this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const organizationPromise = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ organizationSubscriptionPromise, @@ -147,7 +159,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { }; protected updatePaymentMethod = async (): Promise => { - const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, @@ -157,13 +169,13 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogV2ResultType.Submitted) { + if (result === AdjustPaymentDialogResultType.Submitted) { await this.load(); } }; changePayment = async () => { - const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, @@ -171,7 +183,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { }, }); const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogV2ResultType.Submitted) { + if (result === AdjustPaymentDialogResultType.Submitted) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); diff --git a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts index fc7a188f967..c10c9abc9b6 100644 --- a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts @@ -2,11 +2,16 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + InternalOrganizationServiceAbstraction, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationSmSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-sm-subscription-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -107,6 +112,7 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest private platformUtilsService: PlatformUtilsService, private toastService: ToastService, private internalOrganizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { @@ -165,14 +171,19 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest request, ); - const organization = await this.internalOrganizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organization = await firstValueFrom( + this.internalOrganizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const organizationData = new OrganizationData(response, { isMember: organization.isMember, isProviderUser: organization.isProviderUser, }); - await this.internalOrganizationService.upsert(organizationData); + await this.internalOrganizationService.upsert(organizationData, userId); this.toastService.showToast({ variant: "success", diff --git a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts index 7ad0895809c..617b68abb37 100644 --- a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts +++ b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts @@ -2,12 +2,15 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request"; import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; @@ -37,6 +40,7 @@ export class SecretsManagerSubscribeStandaloneComponent { private organizationApiService: OrganizationApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, private toastService: ToastService, + private accountService: AccountService, ) {} submit = async () => { @@ -56,7 +60,8 @@ export class SecretsManagerSubscribeStandaloneComponent { isMember: this.organization.isMember, isProviderUser: this.organization.isProviderUser, }); - await this.organizationService.upsert(organizationData); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + await this.organizationService.upsert(organizationData, userId); /* Because subscribing to Secrets Manager automatically provides access to Secrets Manager for the diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts index cc53e0a32bc..10d7a9c6590 100644 --- a/apps/web/src/app/billing/services/free-families-policy.service.ts +++ b/apps/web/src/app/billing/services/free-families-policy.service.ts @@ -1,11 +1,12 @@ import { Injectable } from "@angular/core"; -import { combineLatest, filter, from, map, Observable, of, switchMap } from "rxjs"; +import { combineLatest, filter, map, Observable, of, switchMap } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; interface EnterpriseOrgStatus { @@ -25,23 +26,38 @@ export class FreeFamiliesPolicyService { constructor( private policyService: PolicyService, private organizationService: OrganizationService, + private accountService: AccountService, private configService: ConfigService, ) {} + canManageSponsorships$ = this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account?.id) { + return this.organizationService.canManageSponsorships$(account?.id); + } else { + return of(); + } + }), + ); + + organizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account?.id) { + return this.organizationService.organizations$(account?.id); + } else { + return of(); + } + }), + ); + get showFreeFamilies$(): Observable { - return this.isFreeFamilyFlagEnabled$.pipe( - switchMap((isFreeFamilyFlagEnabled) => - isFreeFamilyFlagEnabled - ? this.getFreeFamiliesVisibility$() - : this.organizationService.canManageSponsorships$, - ), - ); + return this.getFreeFamiliesVisibility$(); } private getFreeFamiliesVisibility$(): Observable { return combineLatest([ this.checkEnterpriseOrganizationsAndFetchPolicy(), - this.organizationService.canManageSponsorships$, + this.canManageSponsorships$, ]).pipe( map(([orgStatus, canManageSponsorships]) => this.shouldShowFreeFamilyLink(orgStatus, canManageSponsorships), @@ -61,7 +77,7 @@ export class FreeFamiliesPolicyService { } checkEnterpriseOrganizationsAndFetchPolicy(): Observable { - return this.organizationService.organizations$.pipe( + return this.organizations$.pipe( filter((organizations) => Array.isArray(organizations) && organizations.length > 0), switchMap((organizations) => this.fetchEnterpriseOrganizationPolicy(organizations)), ); @@ -90,7 +106,11 @@ export class FreeFamiliesPolicyService { }); } - return this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy).pipe( + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + ), map((policies) => ({ isFreeFamilyPolicyEnabled: policies.some( (policy) => policy.organizationId === organizationId && policy.enabled, @@ -118,8 +138,4 @@ export class FreeFamiliesPolicyService { const enterpriseOrganizations = organizations.filter((org) => org.canManageSponsorships); return enterpriseOrganizations.length === 1 ? enterpriseOrganizations[0].id : null; } - - private get isFreeFamilyFlagEnabled$(): Observable { - return from(this.configService.getFeatureFlag(FeatureFlag.DisableFreeFamiliesSponsorship)); - } } diff --git a/apps/web/src/app/billing/services/trial-flow.service.ts b/apps/web/src/app/billing/services/trial-flow.service.ts index a3a4ba6bba1..eb08e5bd7ad 100644 --- a/apps/web/src/app/billing/services/trial-flow.service.ts +++ b/apps/web/src/app/billing/services/trial-flow.service.ts @@ -16,11 +16,11 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService } from "@bitwarden/components"; -import { FreeTrial } from "../../core/types/free-trial"; import { ChangePlanDialogResultType, openChangePlanDialog, } from "../organizations/change-plan-dialog.component"; +import { FreeTrial } from "../types/free-trial"; @Injectable({ providedIn: "root" }) export class TrialFlowService { diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.ts b/apps/web/src/app/billing/settings/sponsored-families.component.ts index 5e26e80a30a..c35fd3a2e61 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -19,9 +19,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlanSponsorshipType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -40,7 +39,6 @@ interface RequestSponsorshipForm { }) export class SponsoredFamiliesComponent implements OnInit, OnDestroy { loading = false; - isFreeFamilyFlagEnabled: boolean; availableSponsorshipOrgs$: Observable; activeSponsorshipOrgs$: Observable; @@ -63,7 +61,6 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { private formBuilder: FormBuilder, private accountService: AccountService, private toastService: ToastService, - private configService: ConfigService, private policyService: PolicyService, private freeFamiliesPolicyService: FreeFamiliesPolicyService, private router: Router, @@ -86,36 +83,28 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { } async ngOnInit() { - this.isFreeFamilyFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.DisableFreeFamiliesSponsorship, + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + await this.preventAccessToFreeFamiliesPage(); + + this.availableSponsorshipOrgs$ = combineLatest([ + this.organizationService.organizations$(userId), + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + ]).pipe( + map(([organizations, policies]) => + organizations + .filter((org) => org.familySponsorshipAvailable) + .map((org) => ({ + organization: org, + isPolicyEnabled: policies.some( + (policy) => policy.organizationId === org.id && policy.enabled, + ), + })) + .filter(({ isPolicyEnabled }) => !isPolicyEnabled) + .map(({ organization }) => organization), + ), ); - if (this.isFreeFamilyFlagEnabled) { - await this.preventAccessToFreeFamiliesPage(); - - this.availableSponsorshipOrgs$ = combineLatest([ - this.organizationService.organizations$, - this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy), - ]).pipe( - map(([organizations, policies]) => - organizations - .filter((org) => org.familySponsorshipAvailable) - .map((org) => ({ - organization: org, - isPolicyEnabled: policies.some( - (policy) => policy.organizationId === org.id && policy.enabled, - ), - })) - .filter(({ isPolicyEnabled }) => !isPolicyEnabled) - .map(({ organization }) => organization), - ), - ); - } else { - this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable)), - ); - } - this.availableSponsorshipOrgs$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { if (orgs.length === 1) { this.sponsorshipForm.patchValue({ @@ -126,9 +115,9 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { this.anyOrgsAvailable$ = this.availableSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); - this.activeSponsorshipOrgs$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null)), - ); + this.activeSponsorshipOrgs$ = this.organizationService + .organizations$(userId) + .pipe(map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null))); this.anyActiveSponsorships$ = this.activeSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts index b40902112c8..e613b862922 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts @@ -2,13 +2,14 @@ // @ts-strict-ignore import { formatDate } from "@angular/common"; import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core"; -import { firstValueFrom, map, Observable } from "rxjs"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -27,7 +28,6 @@ export class SponsoringOrgRowComponent implements OnInit { statusMessage = "loading"; statusClass: "tw-text-success" | "tw-text-danger" = "tw-text-success"; isFreeFamilyPolicyEnabled$: Observable; - isFreeFamilyFlagEnabled: boolean; private locale = ""; constructor( @@ -38,6 +38,7 @@ export class SponsoringOrgRowComponent implements OnInit { private toastService: ToastService, private configService: ConfigService, private policyService: PolicyService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -49,23 +50,20 @@ export class SponsoringOrgRowComponent implements OnInit { this.sponsoringOrg.familySponsorshipValidUntil, this.sponsoringOrg.familySponsorshipLastSyncDate, ); - this.isFreeFamilyFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.DisableFreeFamiliesSponsorship, - ); - if (this.isFreeFamilyFlagEnabled) { - this.isFreeFamilyPolicyEnabled$ = this.policyService - .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy) - .pipe( - map( - (policies) => - Array.isArray(policies) && - policies.some( - (policy) => policy.organizationId === this.sponsoringOrg.id && policy.enabled, - ), + this.isFreeFamilyPolicyEnabled$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + ), + map( + (policies) => + Array.isArray(policies) && + policies.some( + (policy) => policy.organizationId === this.sponsoringOrg.id && policy.enabled, ), - ); - } + ), + ); } async revokeSponsorship() { diff --git a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts index 71afde81ee3..7860d456685 100644 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts @@ -6,7 +6,10 @@ import { FormControl, FormGroup, Validators } from "@angular/forms"; import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; @@ -77,7 +80,14 @@ export class AddCreditDialogComponent implements OnInit { this.creditAmount = "20.00"; } this.ppButtonCustomField = "organization_id:" + this.organizationId; - const org = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const org = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); if (org != null) { this.subject = org.name; this.name = org.name; diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html deleted file mode 100644 index bb06f87ca03..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts deleted file mode 100644 index 0a72b8302bc..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.ts +++ /dev/null @@ -1,177 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, forwardRef, Inject, OnInit, ViewChild } from "@angular/core"; - -import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PaymentMethodType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { TaxInformation } from "@bitwarden/common/billing/models/domain"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import { PaymentV2Component } from "../payment/payment-v2.component"; - -export interface AdjustPaymentDialogV2Params { - initialPaymentMethod?: PaymentMethodType; - organizationId?: string; - productTier?: ProductTierType; -} - -export enum AdjustPaymentDialogV2ResultType { - Closed = "closed", - Submitted = "submitted", -} - -@Component({ - templateUrl: "./adjust-payment-dialog-v2.component.html", -}) -export class AdjustPaymentDialogV2Component implements OnInit { - @ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component; - @ViewChild(forwardRef(() => ManageTaxInformationComponent)) - taxInfoComponent: ManageTaxInformationComponent; - - protected readonly PaymentMethodType = PaymentMethodType; - protected readonly ResultType = AdjustPaymentDialogV2ResultType; - - protected dialogHeader: string; - protected initialPaymentMethod: PaymentMethodType; - protected organizationId?: string; - protected productTier?: ProductTierType; - - protected taxInformation: TaxInformation; - - constructor( - private apiService: ApiService, - private billingApiService: BillingApiServiceAbstraction, - private organizationApiService: OrganizationApiServiceAbstraction, - @Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogV2Params, - private dialogRef: DialogRef, - private i18nService: I18nService, - private toastService: ToastService, - ) { - const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod"; - this.dialogHeader = this.i18nService.t(key); - this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; - this.organizationId = this.dialogParams.organizationId; - this.productTier = this.dialogParams.productTier; - } - - ngOnInit(): void { - if (this.organizationId) { - this.organizationApiService - .getTaxInfo(this.organizationId) - .then((response: TaxInfoResponse) => { - this.taxInformation = TaxInformation.from(response); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }); - } else { - this.apiService - .getTaxInfo() - .then((response: TaxInfoResponse) => { - this.taxInformation = TaxInformation.from(response); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }); - } - } - - taxInformationChanged(event: TaxInformation) { - this.taxInformation = event; - if (event.country === "US") { - this.paymentComponent.showBankAccount = !!this.organizationId; - } else { - this.paymentComponent.showBankAccount = false; - if (this.paymentComponent.selected === PaymentMethodType.BankAccount) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - } - - submit = async (): Promise => { - if (!this.taxInfoComponent.validate()) { - return; - } - - try { - if (!this.organizationId) { - await this.updatePremiumUserPaymentMethod(); - } else { - await this.updateOrganizationPaymentMethod(); - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("updatedPaymentMethod"), - }); - - this.dialogRef.close(AdjustPaymentDialogV2ResultType.Submitted); - } catch (error) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t(error.message) || error.message, - }); - } - }; - - private updateOrganizationPaymentMethod = async () => { - const paymentSource = await this.paymentComponent.tokenize(); - - const request = new UpdatePaymentMethodRequest(); - request.paymentSource = paymentSource; - request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); - - await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request); - }; - - protected get showTaxIdField(): boolean { - if (!this.organizationId) { - return false; - } - - switch (this.productTier) { - case ProductTierType.Free: - case ProductTierType.Families: - return false; - default: - return true; - } - } - - private updatePremiumUserPaymentMethod = async () => { - const { type, token } = await this.paymentComponent.tokenize(); - - const request = new PaymentRequest(); - request.paymentMethodType = type; - request.paymentToken = token; - request.country = this.taxInformation.country; - request.postalCode = this.taxInformation.postalCode; - request.taxId = this.taxInformation.taxId; - request.state = this.taxInformation.state; - request.line1 = this.taxInformation.line1; - request.line2 = this.taxInformation.line2; - request.city = this.taxInformation.city; - request.state = this.taxInformation.state; - await this.apiService.postAccountPayment(request); - }; - - static open = ( - dialogService: DialogService, - dialogConfig: DialogConfig, - ) => - dialogService.open( - AdjustPaymentDialogV2Component, - dialogConfig, - ); -} diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html index de607314354..4f7990f11a3 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html @@ -1,30 +1,29 @@ - - - - - - - - - - - - + + + + + + + + + + diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index bbae5099afa..0fc49b2ddc1 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -1,59 +1,66 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject, OnInit, ViewChild } from "@angular/core"; -import { FormGroup } from "@angular/forms"; +import { Component, forwardRef, Inject, OnInit, ViewChild } from "@angular/core"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType, ProductTierType } from "@bitwarden/common/billing/enums"; import { TaxInformation } from "@bitwarden/common/billing/models/domain"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; +import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { PaymentComponent } from "../payment/payment.component"; -export interface AdjustPaymentDialogData { - organizationId: string; - currentType: PaymentMethodType; +export interface AdjustPaymentDialogParams { + initialPaymentMethod?: PaymentMethodType; + organizationId?: string; + productTier?: ProductTierType; } -export enum AdjustPaymentDialogResult { - Adjusted = "adjusted", - Cancelled = "cancelled", +export enum AdjustPaymentDialogResultType { + Closed = "closed", + Submitted = "submitted", } @Component({ - templateUrl: "adjust-payment-dialog.component.html", + templateUrl: "./adjust-payment-dialog.component.html", }) export class AdjustPaymentDialogComponent implements OnInit { - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent; + @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; + @ViewChild(forwardRef(() => ManageTaxInformationComponent)) + taxInfoComponent: ManageTaxInformationComponent; - organizationId: string; - currentType: PaymentMethodType; - paymentMethodType = PaymentMethodType; + protected readonly PaymentMethodType = PaymentMethodType; + protected readonly ResultType = AdjustPaymentDialogResultType; - protected DialogResult = AdjustPaymentDialogResult; - protected formGroup = new FormGroup({}); + protected dialogHeader: string; + protected initialPaymentMethod: PaymentMethodType; + protected organizationId?: string; + protected productTier?: ProductTierType; protected taxInformation: TaxInformation; constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData, private apiService: ApiService, - private i18nService: I18nService, + private billingApiService: BillingApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigService, + @Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, private toastService: ToastService, ) { - this.organizationId = data.organizationId; - this.currentType = data.currentType; + const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod"; + this.dialogHeader = this.i18nService.t(key); + this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; + this.organizationId = this.dialogParams.organizationId; + this.productTier = this.dialogParams.productTier; } ngOnInit(): void { @@ -78,65 +85,92 @@ export class AdjustPaymentDialogComponent implements OnInit { } } - submit = async () => { - if (!this.taxInfoComponent?.validate()) { - return; - } - - const request = new PaymentRequest(); - const response = this.paymentComponent.createPaymentToken().then((result) => { - request.paymentToken = result[0]; - request.paymentMethodType = result[1]; - request.postalCode = this.taxInformation?.postalCode; - request.country = this.taxInformation?.country; - request.taxId = this.taxInformation?.taxId; - if (this.organizationId == null) { - return this.apiService.postAccountPayment(request); - } else { - request.taxId = this.taxInformation?.taxId; - request.state = this.taxInformation?.state; - request.line1 = this.taxInformation?.line1; - request.line2 = this.taxInformation?.line2; - request.city = this.taxInformation?.city; - request.state = this.taxInformation?.state; - return this.organizationApiService.updatePayment(this.organizationId, request); - } - }); - await response; - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("updatedPaymentMethod"), - }); - this.dialogRef.close(AdjustPaymentDialogResult.Adjusted); - }; - taxInformationChanged(event: TaxInformation) { this.taxInformation = event; if (event.country === "US") { - this.paymentComponent.hideBank = !this.organizationId; + this.paymentComponent.showBankAccount = !!this.organizationId; } else { - this.paymentComponent.hideBank = true; - if (this.paymentComponent.method === PaymentMethodType.BankAccount) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); + this.paymentComponent.showBankAccount = false; + if (this.paymentComponent.selected === PaymentMethodType.BankAccount) { + this.paymentComponent.select(PaymentMethodType.Card); } } } - protected get showTaxIdField(): boolean { - return !!this.organizationId; - } -} + submit = async (): Promise => { + if (!this.taxInfoComponent.validate()) { + this.taxInfoComponent.markAllAsTouched(); + return; + } -/** - * Strongly typed helper to open a AdjustPaymentDialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ -export function openAdjustPaymentDialog( - dialogService: DialogService, - config: DialogConfig, -) { - return dialogService.open(AdjustPaymentDialogComponent, config); + try { + if (!this.organizationId) { + await this.updatePremiumUserPaymentMethod(); + } else { + await this.updateOrganizationPaymentMethod(); + } + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedPaymentMethod"), + }); + + this.dialogRef.close(AdjustPaymentDialogResultType.Submitted); + } catch (error) { + const msg = typeof error == "object" ? error.message : error; + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t(msg) || msg, + }); + } + }; + + private updateOrganizationPaymentMethod = async () => { + const paymentSource = await this.paymentComponent.tokenize(); + + const request = new UpdatePaymentMethodRequest(); + request.paymentSource = paymentSource; + request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); + + await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request); + }; + + protected get showTaxIdField(): boolean { + if (!this.organizationId) { + return false; + } + + switch (this.productTier) { + case ProductTierType.Free: + case ProductTierType.Families: + return false; + default: + return true; + } + } + + private updatePremiumUserPaymentMethod = async () => { + const { type, token } = await this.paymentComponent.tokenize(); + + const request = new PaymentRequest(); + request.paymentMethodType = type; + request.paymentToken = token; + request.country = this.taxInformation.country; + request.postalCode = this.taxInformation.postalCode; + request.taxId = this.taxInformation.taxId; + request.state = this.taxInformation.state; + request.line1 = this.taxInformation.line1; + request.line2 = this.taxInformation.line2; + request.city = this.taxInformation.city; + request.state = this.taxInformation.state; + await this.apiService.postAccountPayment(request); + }; + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open(AdjustPaymentDialogComponent, dialogConfig); } diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html deleted file mode 100644 index 7b74379acb6..00000000000 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html +++ /dev/null @@ -1,34 +0,0 @@ -
- - -

{{ body }}

-
- - {{ storageFieldLabel }} - - - - {{ "total" | i18n }} - {{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} = - {{ this.price * this.formGroup.value.storage | currency: "$" }} / - {{ this.cadence | i18n }} - - -
-
- - - - -
-
diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts deleted file mode 100644 index ba7619729bf..00000000000 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts +++ /dev/null @@ -1,106 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { StorageRequest } from "@bitwarden/common/models/request/storage.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService, ToastService } from "@bitwarden/components"; - -export interface AdjustStorageDialogV2Params { - price: number; - cadence: "month" | "year"; - type: "Add" | "Remove"; - organizationId?: string; -} - -export enum AdjustStorageDialogV2ResultType { - Submitted = "submitted", - Closed = "closed", -} - -@Component({ - templateUrl: "./adjust-storage-dialog-v2.component.html", -}) -export class AdjustStorageDialogV2Component { - protected formGroup = new FormGroup({ - storage: new FormControl(0, [ - Validators.required, - Validators.min(0), - Validators.max(99), - ]), - }); - - protected organizationId?: string; - protected price: number; - protected cadence: "month" | "year"; - - protected title: string; - protected body: string; - protected storageFieldLabel: string; - - protected ResultType = AdjustStorageDialogV2ResultType; - - constructor( - private apiService: ApiService, - @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogV2Params, - private dialogRef: DialogRef, - private i18nService: I18nService, - private organizationApiService: OrganizationApiServiceAbstraction, - private toastService: ToastService, - ) { - this.price = this.dialogParams.price; - this.cadence = this.dialogParams.cadence; - this.organizationId = this.dialogParams.organizationId; - switch (this.dialogParams.type) { - case "Add": - this.title = this.i18nService.t("addStorage"); - this.body = this.i18nService.t("storageAddNote"); - this.storageFieldLabel = this.i18nService.t("gbStorageAdd"); - break; - case "Remove": - this.title = this.i18nService.t("removeStorage"); - this.body = this.i18nService.t("storageRemoveNote"); - this.storageFieldLabel = this.i18nService.t("gbStorageRemove"); - break; - } - } - - submit = async () => { - const request = new StorageRequest(); - switch (this.dialogParams.type) { - case "Add": - request.storageGbAdjustment = this.formGroup.value.storage; - break; - case "Remove": - request.storageGbAdjustment = this.formGroup.value.storage * -1; - break; - } - - if (this.organizationId) { - await this.organizationApiService.updateStorage(this.organizationId, request); - } else { - await this.apiService.postAccountStorage(request); - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - }); - - this.dialogRef.close(this.ResultType.Submitted); - }; - - static open = ( - dialogService: DialogService, - dialogConfig: DialogConfig, - ) => - dialogService.open( - AdjustStorageDialogV2Component, - dialogConfig, - ); -} diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html index a597a3ae5ea..832356477c4 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html @@ -1,17 +1,17 @@
- + -

{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}

+

{{ body }}

- {{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }} - - - {{ "total" | i18n }}: - {{ formGroup.get("storageAdjustment").value || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ - interval | i18n - }} + {{ storageFieldLabel }} + + + + {{ "total" | i18n }} + {{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} = + {{ this.price * this.formGroup.value.storage | currency: "$" }} / + {{ this.cadence | i18n }}
@@ -25,11 +25,10 @@ bitButton bitFormButton buttonType="secondary" - [bitDialogClose]="DialogResult.Cancelled" + [bitDialogClose]="ResultType.Closed" > {{ "cancel" | i18n }}
- diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts index f69f9e3eaad..4362e36f857 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts @@ -1,132 +1,103 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject, ViewChild } from "@angular/core"; +import { Component, Inject } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PaymentResponse } from "@bitwarden/common/billing/models/response/payment.response"; import { StorageRequest } from "@bitwarden/common/models/request/storage.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PaymentComponent } from "../payment/payment.component"; - -export interface AdjustStorageDialogData { - storageGbPrice: number; - add: boolean; +export interface AdjustStorageDialogParams { + price: number; + cadence: "month" | "year"; + type: "Add" | "Remove"; organizationId?: string; - interval?: string; } -export enum AdjustStorageDialogResult { - Adjusted = "adjusted", - Cancelled = "cancelled", +export enum AdjustStorageDialogResultType { + Submitted = "submitted", + Closed = "closed", } @Component({ - templateUrl: "adjust-storage-dialog.component.html", + templateUrl: "./adjust-storage-dialog.component.html", }) export class AdjustStorageDialogComponent { - storageGbPrice: number; - add: boolean; - organizationId: string; - interval: string; - - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - - protected DialogResult = AdjustStorageDialogResult; protected formGroup = new FormGroup({ - storageAdjustment: new FormControl(0, [ + storage: new FormControl(0, [ Validators.required, Validators.min(0), Validators.max(99), ]), }); + protected organizationId?: string; + protected price: number; + protected cadence: "month" | "year"; + + protected title: string; + protected body: string; + protected storageFieldLabel: string; + + protected ResultType = AdjustStorageDialogResultType; + constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AdjustStorageDialogData, private apiService: ApiService, + @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogParams, + private dialogRef: DialogRef, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private router: Router, - private activatedRoute: ActivatedRoute, - private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, private toastService: ToastService, ) { - this.storageGbPrice = data.storageGbPrice; - this.add = data.add; - this.organizationId = data.organizationId; - this.interval = data.interval || "year"; + this.price = this.dialogParams.price; + this.cadence = this.dialogParams.cadence; + this.organizationId = this.dialogParams.organizationId; + switch (this.dialogParams.type) { + case "Add": + this.title = this.i18nService.t("addStorage"); + this.body = this.i18nService.t("storageAddNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageAdd"); + break; + case "Remove": + this.title = this.i18nService.t("removeStorage"); + this.body = this.i18nService.t("storageRemoveNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageRemove"); + break; + } } submit = async () => { const request = new StorageRequest(); - request.storageGbAdjustment = this.formGroup.value.storageAdjustment; - if (!this.add) { - request.storageGbAdjustment *= -1; + switch (this.dialogParams.type) { + case "Add": + request.storageGbAdjustment = this.formGroup.value.storage; + break; + case "Remove": + request.storageGbAdjustment = this.formGroup.value.storage * -1; + break; } - let paymentFailed = false; - const action = async () => { - let response: Promise; - if (this.organizationId == null) { - response = this.apiService.postAccountStorage(request); - } else { - response = this.organizationApiService.updateStorage(this.organizationId, request); - } - const result = await response; - if (result != null && result.paymentIntentClientSecret != null) { - try { - await this.paymentComponent.handleStripeCardPayment( - result.paymentIntentClientSecret, - null, - ); - } catch { - paymentFailed = true; - } - } - }; - await action(); - this.dialogRef.close(AdjustStorageDialogResult.Adjusted); - if (paymentFailed) { - this.toastService.showToast({ - variant: "warning", - title: null, - message: this.i18nService.t("couldNotChargeCardPayInvoice"), - timeout: 10000, - }); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["../billing"], { relativeTo: this.activatedRoute }); + if (this.organizationId) { + await this.organizationApiService.updateStorage(this.organizationId, request); } else { - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - }); + await this.apiService.postAccountStorage(request); } + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), + }); + + this.dialogRef.close(this.ResultType.Submitted); }; - get adjustedStorageTotal(): number { - return this.storageGbPrice * this.formGroup.value.storageAdjustment; - } -} - -/** - * Strongly typed helper to open an AdjustStorageDialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ -export function openAdjustStorageDialog( - dialogService: DialogService, - config: DialogConfig, -) { - return dialogService.open(AdjustStorageDialogComponent, config); + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open(AdjustStorageDialogComponent, dialogConfig); } diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index b9c235943ad..9a69755b209 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -6,13 +6,10 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; import { AddCreditDialogComponent } from "./add-credit-dialog.component"; -import { AdjustPaymentDialogV2Component } from "./adjust-payment-dialog/adjust-payment-dialog-v2.component"; import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component"; -import { AdjustStorageDialogV2Component } from "./adjust-storage-dialog/adjust-storage-dialog-v2.component"; import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; -import { PaymentV2Component } from "./payment/payment-v2.component"; import { PaymentComponent } from "./payment/payment.component"; import { PaymentMethodComponent } from "./payment-method.component"; import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component"; @@ -26,40 +23,35 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac @NgModule({ imports: [ SharedModule, - PaymentComponent, TaxInfoComponent, HeaderModule, BannerModule, - PaymentV2Component, + PaymentComponent, VerifyBankAccountComponent, ], declarations: [ AddCreditDialogComponent, - AdjustPaymentDialogComponent, - AdjustStorageDialogComponent, BillingHistoryComponent, PaymentMethodComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, - AdjustPaymentDialogV2Component, - AdjustStorageDialogV2Component, + AdjustPaymentDialogComponent, + AdjustStorageDialogComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, ], exports: [ SharedModule, - PaymentComponent, TaxInfoComponent, - AdjustStorageDialogComponent, BillingHistoryComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, VerifyBankAccountComponent, - PaymentV2Component, + PaymentComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, ], diff --git a/apps/web/src/app/billing/shared/index.ts b/apps/web/src/app/billing/shared/index.ts index 69a4b93bec8..54ab5bc0a2a 100644 --- a/apps/web/src/app/billing/shared/index.ts +++ b/apps/web/src/app/billing/shared/index.ts @@ -1,5 +1,4 @@ export * from "./billing-shared.module"; export * from "./payment-method.component"; -export * from "./payment/payment.component"; export * from "./sm-subscribe.component"; export * from "./tax-info.component"; diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 149b4adf520..dc031ade42f 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -4,12 +4,16 @@ import { Location } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { lastValueFrom } from "rxjs"; +import { firstValueFrom, lastValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BillingPaymentResponse } from "@bitwarden/common/billing/models/response/billing-payment.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -20,13 +24,13 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; -import { FreeTrial } from "../../core/types/free-trial"; import { TrialFlowService } from "../services/trial-flow.service"; +import { FreeTrial } from "../types/free-trial"; import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; import { - AdjustPaymentDialogResult, - openAdjustPaymentDialog, + AdjustPaymentDialogComponent, + AdjustPaymentDialogResultType, } from "./adjust-payment-dialog/adjust-payment-dialog.component"; @Component({ @@ -73,6 +77,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { private toastService: ToastService, private trialFlowService: TrialFlowService, private organizationService: OrganizationService, + private accountService: AccountService, protected syncService: SyncService, ) { const state = this.router.getCurrentNavigation()?.extras?.state; @@ -117,7 +122,14 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); - const organizationPromise = this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const organizationPromise = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); [this.billing, this.org, this.organization] = await Promise.all([ billingPromise, @@ -158,14 +170,16 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { }; changePayment = async () => { - const dialogRef = openAdjustPaymentDialog(this.dialogService, { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { organizationId: this.organizationId, - currentType: this.paymentSource !== null ? this.paymentSource.type : null, + initialPaymentMethod: this.paymentSource !== null ? this.paymentSource.type : null, }, }); + const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogResult.Adjusted) { + + if (result === AdjustPaymentDialogResultType.Submitted) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); diff --git a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.html b/apps/web/src/app/billing/shared/payment/payment-label.component.html similarity index 100% rename from apps/web/src/app/billing/shared/payment/payment-label-v2.component.html rename to apps/web/src/app/billing/shared/payment/payment-label.component.html diff --git a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-label.component.ts similarity index 89% rename from apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts rename to apps/web/src/app/billing/shared/payment/payment-label.component.ts index f4d0f097766..179011e1144 100644 --- a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment-label.component.ts @@ -15,12 +15,12 @@ import { SharedModule } from "../../../shared"; * the `ExtensionRefresh` flag is set. */ @Component({ - selector: "app-payment-label-v2", - templateUrl: "./payment-label-v2.component.html", + selector: "app-payment-label", + templateUrl: "./payment-label.component.html", standalone: true, imports: [FormFieldModule, SharedModule], }) -export class PaymentLabelV2 implements OnInit { +export class PaymentLabelComponent implements OnInit { /** `id` of the associated input */ @Input({ required: true }) for: string; /** Displays required text on the label */ diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.html b/apps/web/src/app/billing/shared/payment/payment-v2.component.html deleted file mode 100644 index 9804e6bc86f..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.html +++ /dev/null @@ -1,152 +0,0 @@ -
-
- - - - - {{ "creditCard" | i18n }} - - - - - - {{ "bankAccount" | i18n }} - - - - - - {{ "payPal" | i18n }} - - - - - - {{ "accountCredit" | i18n }} - - - -
- - -
-
- - {{ "number" | i18n }} - -
-
-
- Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay -
-
- - {{ "expiration" | i18n }} - -
-
-
- - {{ "securityCodeSlashCVV" | i18n }} - - - - -
-
-
-
- - - - {{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }} - -
- - {{ "routingNumber" | i18n }} - - - - {{ "accountNumber" | i18n }} - - - - {{ "accountHolderName" | i18n }} - - - - {{ "bankAccountType" | i18n }} - - - - - - -
-
- - -
-
- {{ "paypalClickSubmit" | i18n }} -
-
- - - - {{ "makeSureEnoughCredit" | i18n }} - - - -
diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-v2.component.ts deleted file mode 100644 index f65a5743c35..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts +++ /dev/null @@ -1,205 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Subject } from "rxjs"; -import { takeUntil } from "rxjs/operators"; - -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; - -import { SharedModule } from "../../../shared"; -import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; - -import { PaymentLabelV2 } from "./payment-label-v2.component"; - -/** - * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, - * optionally, submit it using the {@link onSubmit} function if it is provided. - * - * This component is meant to replace the existing {@link PaymentComponent} which is using the deprecated Stripe Sources API. - */ -@Component({ - selector: "app-payment-v2", - templateUrl: "./payment-v2.component.html", - standalone: true, - imports: [BillingServicesModule, SharedModule, PaymentLabelV2], -}) -export class PaymentV2Component implements OnInit, OnDestroy { - /** Show account credit as a payment option. */ - @Input() showAccountCredit: boolean = true; - /** Show bank account as a payment option. */ - @Input() showBankAccount: boolean = true; - /** Show PayPal as a payment option. */ - @Input() showPayPal: boolean = true; - - /** The payment method selected by default when the component renders. */ - @Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card; - /** If provided, will be invoked with the tokenized payment source during form submission. */ - @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; - - @Output() submitted = new EventEmitter(); - - private destroy$ = new Subject(); - - protected formGroup = new FormGroup({ - paymentMethod: new FormControl(null), - bankInformation: new FormGroup({ - routingNumber: new FormControl("", [Validators.required]), - accountNumber: new FormControl("", [Validators.required]), - accountHolderName: new FormControl("", [Validators.required]), - accountHolderType: new FormControl("", [Validators.required]), - }), - }); - - protected PaymentMethodType = PaymentMethodType; - - constructor( - private billingApiService: BillingApiServiceAbstraction, - private braintreeService: BraintreeService, - private stripeService: StripeService, - ) {} - - ngOnInit(): void { - this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod); - - this.stripeService.loadStripe( - { - cardNumber: "#stripe-card-number", - cardExpiry: "#stripe-card-expiry", - cardCvc: "#stripe-card-cvc", - }, - this.initialPaymentMethod === PaymentMethodType.Card, - ); - - if (this.showPayPal) { - this.braintreeService.loadBraintree( - "#braintree-container", - this.initialPaymentMethod === PaymentMethodType.PayPal, - ); - } - - this.formGroup - .get("paymentMethod") - .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((type) => { - this.onPaymentMethodChange(type); - }); - } - - /** Programmatically select the provided payment method. */ - select = (paymentMethod: PaymentMethodType) => { - this.formGroup.get("paymentMethod").patchValue(paymentMethod); - }; - - protected submit = async () => { - const { type, token } = await this.tokenize(); - await this.onSubmit?.({ type, token }); - this.submitted.emit(type); - }; - - /** - * Tokenize the payment method information entered by the user against one of our payment providers. - * - * - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} - * - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} - * - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} - * */ - async tokenize(): Promise<{ type: PaymentMethodType; token: string }> { - const type = this.selected; - - if (this.usingStripe) { - const clientSecret = await this.billingApiService.createSetupIntent(type); - - if (this.usingBankAccount) { - this.formGroup.markAllAsTouched(); - if (this.formGroup.valid) { - const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { - accountHolderName: this.formGroup.value.bankInformation.accountHolderName, - routingNumber: this.formGroup.value.bankInformation.routingNumber, - accountNumber: this.formGroup.value.bankInformation.accountNumber, - accountHolderType: this.formGroup.value.bankInformation.accountHolderType, - }); - return { - type, - token, - }; - } else { - throw "Invalid input provided, Please ensure all required fields are filled out correctly and try again."; - } - } - - if (this.usingCard) { - const token = await this.stripeService.setupCardPaymentMethod(clientSecret); - return { - type, - token, - }; - } - } - - if (this.usingPayPal) { - const token = await this.braintreeService.requestPaymentMethod(); - return { - type, - token, - }; - } - - if (this.usingAccountCredit) { - return { - type: PaymentMethodType.Credit, - token: null, - }; - } - - return null; - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - this.stripeService.unloadStripe(); - if (this.showPayPal) { - this.braintreeService.unloadBraintree(); - } - } - - private onPaymentMethodChange(type: PaymentMethodType): void { - switch (type) { - case PaymentMethodType.Card: { - this.stripeService.mountElements(); - break; - } - case PaymentMethodType.PayPal: { - this.braintreeService.createDropin(); - break; - } - } - } - - get selected(): PaymentMethodType { - return this.formGroup.value.paymentMethod; - } - - protected get usingAccountCredit(): boolean { - return this.selected === PaymentMethodType.Credit; - } - - protected get usingBankAccount(): boolean { - return this.selected === PaymentMethodType.BankAccount; - } - - protected get usingCard(): boolean { - return this.selected === PaymentMethodType.Card; - } - - protected get usingPayPal(): boolean { - return this.selected === PaymentMethodType.PayPal; - } - - private get usingStripe(): boolean { - return this.usingBankAccount || this.usingCard; - } -} diff --git a/apps/web/src/app/billing/shared/payment/payment.component.html b/apps/web/src/app/billing/shared/payment/payment.component.html index d4853713579..af261155171 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.html +++ b/apps/web/src/app/billing/shared/payment/payment.component.html @@ -1,96 +1,125 @@ -
-
- - +
+
+ + - {{ "creditCard" | i18n }} + {{ "creditCard" | i18n }} + - + - {{ "bankAccount" | i18n }} + {{ "bankAccount" | i18n }} + - - PayPal + + + + {{ "payPal" | i18n }} + - + - {{ "accountCredit" | i18n }} + {{ "accountCredit" | i18n }} +
- -
-
- {{ - "number" | i18n - }} -
+ + +
+
+ + {{ "number" | i18n }} + +
-
+
Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay
-
- {{ - "expiration" | i18n - }} -
+
+ + {{ "expiration" | i18n }} + +
-
- +
+ {{ "securityCodeSlashCVV" | i18n }} - -
+
+
- + + - {{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }} + {{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }} -
- +
+ {{ "routingNumber" | i18n }} - - - - {{ "accountNumber" | i18n }} - - - - {{ "accountHolderName" | i18n }} - - + + {{ "accountNumber" | i18n }} + + + + {{ "accountHolderName" | i18n }} + + + {{ "bankAccountType" | i18n }} - +
- + +
-
+
{{ "paypalClickSubmit" | i18n }}
- - + + + {{ "makeSureEnoughCredit" | i18n }} - + -
+ + diff --git a/apps/web/src/app/billing/shared/payment/payment.component.ts b/apps/web/src/app/billing/shared/payment/payment.component.ts index e067a5ee490..c11dfddb6cc 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment.component.ts @@ -1,330 +1,203 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; -import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; import { SharedModule } from "../../../shared"; +import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; -import { PaymentLabelV2 } from "./payment-label-v2.component"; +import { PaymentLabelComponent } from "./payment-label.component"; +/** + * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, + * optionally, submit it using the {@link onSubmit} function if it is provided. + */ @Component({ selector: "app-payment", - templateUrl: "payment.component.html", + templateUrl: "./payment.component.html", standalone: true, - imports: [SharedModule, PaymentLabelV2], + imports: [BillingServicesModule, SharedModule, PaymentLabelComponent], }) export class PaymentComponent implements OnInit, OnDestroy { - @Input() showMethods = true; - @Input() showOptions = true; - @Input() hideBank = false; - @Input() hidePaypal = false; - @Input() hideCredit = false; - @Input() trialFlow = false; + /** Show account credit as a payment option. */ + @Input() showAccountCredit: boolean = true; + /** Show bank account as a payment option. */ + @Input() showBankAccount: boolean = true; + /** Show PayPal as a payment option. */ + @Input() showPayPal: boolean = true; - @Input() - set method(value: PaymentMethodType) { - this._method = value; - this.paymentForm?.controls.method.setValue(value, { emitEvent: false }); - } + /** The payment method selected by default when the component renders. */ + @Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card; + /** If provided, will be invoked with the tokenized payment source during form submission. */ + @Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise; - get method(): PaymentMethodType { - return this._method; - } - private _method: PaymentMethodType = PaymentMethodType.Card; + @Output() submitted = new EventEmitter(); private destroy$ = new Subject(); - protected paymentForm = new FormGroup({ - method: new FormControl(this.method), - bank: new FormGroup({ - routing_number: new FormControl(null, [Validators.required]), - account_number: new FormControl(null, [Validators.required]), - account_holder_name: new FormControl(null, [Validators.required]), - account_holder_type: new FormControl("", [Validators.required]), - currency: new FormControl("USD"), - country: new FormControl("US"), + + protected formGroup = new FormGroup({ + paymentMethod: new FormControl(null), + bankInformation: new FormGroup({ + routingNumber: new FormControl("", [Validators.required]), + accountNumber: new FormControl("", [Validators.required]), + accountHolderName: new FormControl("", [Validators.required]), + accountHolderType: new FormControl("", [Validators.required]), }), }); - paymentMethodType = PaymentMethodType; - private btScript: HTMLScriptElement; - private btInstance: any = null; - private stripeScript: HTMLScriptElement; - private stripe: any = null; - private stripeElements: any = null; - private stripeCardNumberElement: any = null; - private stripeCardExpiryElement: any = null; - private stripeCardCvcElement: any = null; - private StripeElementStyle: any; - private StripeElementClasses: any; + protected PaymentMethodType = PaymentMethodType; constructor( - private apiService: ApiService, - private logService: LogService, - private themingService: AbstractThemingService, - private configService: ConfigService, - ) { - this.stripeScript = window.document.createElement("script"); - this.stripeScript.src = "https://js.stripe.com/v3/?advancedFraudSignals=false"; - this.stripeScript.async = true; - this.stripeScript.onload = async () => { - this.stripe = (window as any).Stripe(process.env.STRIPE_KEY); - this.stripeElements = this.stripe.elements(); - await this.setStripeElement(); - }; - this.btScript = window.document.createElement("script"); - this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`; - this.btScript.async = true; - this.StripeElementStyle = { - base: { - color: null, - fontFamily: - '"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' + - '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', - fontSize: "16px", - fontSmoothing: "antialiased", - "::placeholder": { - color: null, - }, + private billingApiService: BillingApiServiceAbstraction, + private braintreeService: BraintreeService, + private stripeService: StripeService, + ) {} + + ngOnInit(): void { + this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod); + + this.stripeService.loadStripe( + { + cardNumber: "#stripe-card-number", + cardExpiry: "#stripe-card-expiry", + cardCvc: "#stripe-card-cvc", }, - invalid: { - color: null, - }, - }; - this.StripeElementClasses = { - focus: "is-focused", - empty: "is-empty", - invalid: "is-invalid", - }; - } - async ngOnInit() { - if (!this.showOptions) { - this.hidePaypal = this.method !== PaymentMethodType.PayPal; - this.hideBank = this.method !== PaymentMethodType.BankAccount; - this.hideCredit = this.method !== PaymentMethodType.Credit; - } - this.subscribeToTheme(); - window.document.head.appendChild(this.stripeScript); - if (!this.hidePaypal) { - window.document.head.appendChild(this.btScript); - } - this.paymentForm - .get("method") - .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((v) => { - this.method = v; - this.changeMethod(); - }); - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - window.document.head.removeChild(this.stripeScript); - window.setTimeout(() => { - Array.from(window.document.querySelectorAll("iframe")).forEach((el) => { - if (el.src != null && el.src.indexOf("stripe") > -1) { - try { - window.document.body.removeChild(el); - } catch (e) { - this.logService.error(e); - } - } - }); - }, 500); - if (!this.hidePaypal) { - window.document.head.removeChild(this.btScript); - window.setTimeout(() => { - Array.from(window.document.head.querySelectorAll("script")).forEach((el) => { - if (el.src != null && el.src.indexOf("paypal") > -1) { - try { - window.document.head.removeChild(el); - } catch (e) { - this.logService.error(e); - } - } - }); - const btStylesheet = window.document.head.querySelector("#braintree-dropin-stylesheet"); - if (btStylesheet != null) { - try { - window.document.head.removeChild(btStylesheet); - } catch (e) { - this.logService.error(e); - } - } - }, 500); - } - } - - changeMethod() { - this.btInstance = null; - if (this.method === PaymentMethodType.PayPal) { - window.setTimeout(() => { - (window as any).braintree.dropin.create( - { - authorization: process.env.BRAINTREE_KEY, - container: "#bt-dropin-container", - paymentOptionPriority: ["paypal"], - paypal: { - flow: "vault", - buttonStyle: { - label: "pay", - size: "medium", - shape: "pill", - color: "blue", - tagline: "false", - }, - }, - }, - (createErr: any, instance: any) => { - if (createErr != null) { - // eslint-disable-next-line - console.error(createErr); - return; - } - this.btInstance = instance; - }, - ); - }, 250); - } else { - void this.setStripeElement(); - } - } - - createPaymentToken(): Promise<[string, PaymentMethodType]> { - return new Promise((resolve, reject) => { - if (this.method === PaymentMethodType.Credit) { - resolve([null, this.method]); - } else if (this.method === PaymentMethodType.PayPal) { - this.btInstance - .requestPaymentMethod() - .then((payload: any) => { - resolve([payload.nonce, this.method]); - }) - .catch((err: any) => { - reject(err.message); - }); - } else if ( - this.method === PaymentMethodType.Card || - this.method === PaymentMethodType.BankAccount - ) { - if (this.method === PaymentMethodType.Card) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.apiService - .postSetupPayment() - .then((clientSecret) => - this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement), - ) - .then((result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.setupIntent && result.setupIntent.status === "succeeded") { - resolve([result.setupIntent.payment_method, this.method]); - } else { - reject(); - } - }); - } else { - this.stripe - .createToken("bank_account", this.paymentForm.get("bank").value) - .then((result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.token && result.token.id != null) { - resolve([result.token.id, this.method]); - } else { - reject(); - } - }); - } - } - }); - } - - handleStripeCardPayment(clientSecret: string, successCallback: () => Promise): Promise { - return new Promise((resolve, reject) => { - if (this.showMethods && this.stripeCardNumberElement == null) { - reject(); - return; - } - const handleCardPayment = () => - this.showMethods - ? this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement) - : this.stripe.handleCardSetup(clientSecret); - return handleCardPayment().then(async (result: any) => { - if (result.error) { - reject(result.error.message); - } else if (result.paymentIntent && result.paymentIntent.status === "succeeded") { - if (successCallback != null) { - await successCallback(); - } - resolve(); - } else { - reject(); - } - }); - }); - } - - private async setStripeElement() { - const extensionRefreshFlag = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, + this.initialPaymentMethod === PaymentMethodType.Card, ); - // Apply unique styles for extension refresh - if (extensionRefreshFlag) { - this.StripeElementStyle.base.fontWeight = "500"; - this.StripeElementClasses.base = "v2"; + if (this.showPayPal) { + this.braintreeService.loadBraintree( + "#braintree-container", + this.initialPaymentMethod === PaymentMethodType.PayPal, + ); } - window.setTimeout(() => { - if (this.showMethods && this.method === PaymentMethodType.Card) { - if (this.stripeCardNumberElement == null) { - this.stripeCardNumberElement = this.stripeElements.create("cardNumber", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - placeholder: "", - }); - } - if (this.stripeCardExpiryElement == null) { - this.stripeCardExpiryElement = this.stripeElements.create("cardExpiry", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - }); - } - if (this.stripeCardCvcElement == null) { - this.stripeCardCvcElement = this.stripeElements.create("cardCvc", { - style: this.StripeElementStyle, - classes: this.StripeElementClasses, - placeholder: "", - }); - } - this.stripeCardNumberElement.mount("#stripe-card-number-element"); - this.stripeCardExpiryElement.mount("#stripe-card-expiry-element"); - this.stripeCardCvcElement.mount("#stripe-card-cvc-element"); - } - }, 50); + this.formGroup + .get("paymentMethod") + .valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe((type) => { + this.onPaymentMethodChange(type); + }); } - private subscribeToTheme() { - this.themingService.theme$.pipe(takeUntil(this.destroy$)).subscribe(() => { - const style = getComputedStyle(document.documentElement); - this.StripeElementStyle.base.color = `rgb(${style.getPropertyValue("--color-text-main")})`; - this.StripeElementStyle.base["::placeholder"].color = `rgb(${style.getPropertyValue( - "--color-text-muted", - )})`; - this.StripeElementStyle.invalid.color = `rgb(${style.getPropertyValue("--color-text-main")})`; - this.StripeElementStyle.invalid.borderColor = `rgb(${style.getPropertyValue( - "--color-danger-600", - )})`; - }); + /** Programmatically select the provided payment method. */ + select = (paymentMethod: PaymentMethodType) => { + this.formGroup.get("paymentMethod").patchValue(paymentMethod); + }; + + protected submit = async () => { + const { type, token } = await this.tokenize(); + await this.onSubmit?.({ type, token }); + this.submitted.emit(type); + }; + + /** + * Tokenize the payment method information entered by the user against one of our payment providers. + * + * - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} + * - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} + * - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} + * */ + async tokenize(): Promise<{ type: PaymentMethodType; token: string }> { + const type = this.selected; + + if (this.usingStripe) { + const clientSecret = await this.billingApiService.createSetupIntent(type); + + if (this.usingBankAccount) { + this.formGroup.markAllAsTouched(); + if (this.formGroup.valid) { + const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { + accountHolderName: this.formGroup.value.bankInformation.accountHolderName, + routingNumber: this.formGroup.value.bankInformation.routingNumber, + accountNumber: this.formGroup.value.bankInformation.accountNumber, + accountHolderType: this.formGroup.value.bankInformation.accountHolderType, + }); + return { + type, + token, + }; + } else { + throw "Invalid input provided, Please ensure all required fields are filled out correctly and try again."; + } + } + + if (this.usingCard) { + const token = await this.stripeService.setupCardPaymentMethod(clientSecret); + return { + type, + token, + }; + } + } + + if (this.usingPayPal) { + const token = await this.braintreeService.requestPaymentMethod(); + return { + type, + token, + }; + } + + if (this.usingAccountCredit) { + return { + type: PaymentMethodType.Credit, + token: null, + }; + } + + return null; + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.stripeService.unloadStripe(); + if (this.showPayPal) { + this.braintreeService.unloadBraintree(); + } + } + + private onPaymentMethodChange(type: PaymentMethodType): void { + switch (type) { + case PaymentMethodType.Card: { + this.stripeService.mountElements(); + break; + } + case PaymentMethodType.PayPal: { + this.braintreeService.createDropin(); + break; + } + } + } + + get selected(): PaymentMethodType { + return this.formGroup.value.paymentMethod; + } + + protected get usingAccountCredit(): boolean { + return this.selected === PaymentMethodType.Credit; + } + + protected get usingBankAccount(): boolean { + return this.selected === PaymentMethodType.BankAccount; + } + + protected get usingCard(): boolean { + return this.selected === PaymentMethodType.Card; + } + + protected get usingPayPal(): boolean { + return this.selected === PaymentMethodType.PayPal; + } + + private get usingStripe(): boolean { + return this.usingBankAccount || this.usingCard; } } diff --git a/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts b/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts index 41cc977d46f..c8d5eac2099 100644 --- a/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts +++ b/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts @@ -7,7 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html rename to apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts similarity index 99% rename from apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts rename to apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts index 96c50a81319..873ceea2ada 100644 --- a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts @@ -25,13 +25,13 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; +import { AcceptOrganizationInviteService } from "../../../auth/organization-invite/accept-organization.service"; import { OrganizationCreatedEvent, SubscriptionProduct, TrialOrganizationType, } from "../../../billing/accounts/trial-initiation/trial-billing-step.component"; import { RouterService } from "../../../core/router.service"; -import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service"; import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component"; export type InitiationPath = diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts rename to apps/web/src/app/billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts rename to apps/web/src/app/billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts diff --git a/apps/web/src/app/auth/trial-initiation/confirmation-details.component.html b/apps/web/src/app/billing/trial-initiation/confirmation-details.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/confirmation-details.component.html rename to apps/web/src/app/billing/trial-initiation/confirmation-details.component.html diff --git a/apps/web/src/app/auth/trial-initiation/confirmation-details.component.ts b/apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/confirmation-details.component.ts rename to apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/abm-enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/abm-enterprise-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/abm-enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/abm-enterprise-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/abm-enterprise-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/abm-teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/abm-teams-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/abm-teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/abm-teams-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/abm-teams-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-enterprise-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-enterprise-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/cnet-enterprise-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-individual-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-individual-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-individual-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-individual-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/cnet-individual-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-teams-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/cnet-teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/cnet-teams-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/cnet-teams-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/default-content.component.html b/apps/web/src/app/billing/trial-initiation/content/default-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/default-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/default-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/default-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/default-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/default-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/default-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/enterprise-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/enterprise1-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html b/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/enterprise2-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-badges.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-badges.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-badges.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-badges.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-badges.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-badges.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-cnet-5-stars.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-cnet-5-stars.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-cnet-5-stars.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-cnet-5-stars.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-cnet-5-stars.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-cnet.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-cnet.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-cnet.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-cnet.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-cnet.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-company-testimonial.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-forbes.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-forbes.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-forbes.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-forbes.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-forbes.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-us-news.component.html b/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-us-news.component.html rename to apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-us-news.component.ts b/apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/logo-us-news.component.ts rename to apps/web/src/app/billing/trial-initiation/content/logo-us-news.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/review-blurb.component.html b/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/review-blurb.component.html rename to apps/web/src/app/billing/trial-initiation/content/review-blurb.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/review-blurb.component.ts b/apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/review-blurb.component.ts rename to apps/web/src/app/billing/trial-initiation/content/review-blurb.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/review-logo.component.html b/apps/web/src/app/billing/trial-initiation/content/review-logo.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/review-logo.component.html rename to apps/web/src/app/billing/trial-initiation/content/review-logo.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/review-logo.component.ts b/apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/review-logo.component.ts rename to apps/web/src/app/billing/trial-initiation/content/review-logo.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/secrets-manager-content.component.html b/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/secrets-manager-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/secrets-manager-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/secrets-manager-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/secrets-manager-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/teams-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/teams-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/teams-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/teams-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/teams1-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams1-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/teams1-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/teams2-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams2-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/teams2-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/content/teams3-content.component.html b/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams3-content.component.html rename to apps/web/src/app/billing/trial-initiation/content/teams3-content.component.html diff --git a/apps/web/src/app/auth/trial-initiation/content/teams3-content.component.ts b/apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts similarity index 100% rename from apps/web/src/app/auth/trial-initiation/content/teams3-content.component.ts rename to apps/web/src/app/billing/trial-initiation/content/teams3-content.component.ts diff --git a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html similarity index 78% rename from apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html rename to apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html index 0b6e44d4eb6..dddac598a46 100644 --- a/apps/web/src/app/auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html +++ b/apps/web/src/app/billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component.html @@ -1,17 +1,4 @@ - - - - - - - - diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 8f21dfa2c8b..be42a9ba34e 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -52,10 +52,10 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService, Urls, @@ -66,14 +66,22 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; +// eslint-disable-next-line no-restricted-imports -- Needed for DI +import { + UnsupportedWebPushConnectionService, + WebPushConnectionService, +} from "@bitwarden/common/platform/notifications/internal"; import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; +import { NoopSdkLoadService } from "@bitwarden/common/platform/services/sdk/noop-sdk-load.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; /* eslint-disable import/no-restricted-paths -- Implementation for memory storage */ import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; @@ -91,7 +99,7 @@ import { KeyService as KeyServiceAbstraction, BiometricsService, } from "@bitwarden/key-management"; -import { LockComponentService } from "@bitwarden/key-management/angular"; +import { LockComponentService } from "@bitwarden/key-management-ui"; import { flagEnabled } from "../../utils/flags"; import { PolicyListService } from "../admin-console/core/policy-list.service"; @@ -110,7 +118,7 @@ import { WebProcessReloadService } from "../key-management/services/web-process- import { WebBiometricsService } from "../key-management/web-biometric.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; import { WebMigrationRunner } from "../platform/web-migration-runner"; -import { WebSdkClientFactory } from "../platform/web-sdk-client-factory"; +import { WebSdkLoadService } from "../platform/web-sdk-load.service"; import { WebStorageServiceProvider } from "../platform/web-storage-service.provider"; import { EventService } from "./event.service"; @@ -242,6 +250,12 @@ const safeProviders: SafeProvider[] = [ PolicyService, ], }), + safeProvider({ + provide: WebPushConnectionService, + // We can support web in the future by creating a worker + useClass: UnsupportedWebPushConnectionService, + deps: [], + }), safeProvider({ provide: LockComponentService, useClass: WebLockComponentService, @@ -288,9 +302,14 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultCollectionAdminService, deps: [ApiService, KeyServiceAbstraction, EncryptService, CollectionService], }), + safeProvider({ + provide: SdkLoadService, + useClass: flagEnabled("sdk") ? WebSdkLoadService : NoopSdkLoadService, + deps: [], + }), safeProvider({ provide: SdkClientFactory, - useClass: flagEnabled("sdk") ? WebSdkClientFactory : NoopSdkClientFactory, + useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory, deps: [], }), safeProvider({ diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index b3e6d691f75..3623d9b0d2f 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -5,12 +5,13 @@ import { firstValueFrom } from "rxjs"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; -import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; +import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; @@ -23,7 +24,7 @@ import { VersionService } from "../platform/version.service"; export class InitService { constructor( @Inject(WINDOW) private win: Window, - private notificationsService: NotificationsServiceAbstraction, + private notificationsService: NotificationsService, private vaultTimeoutService: VaultTimeoutService, private i18nService: I18nServiceAbstraction, private eventUploadService: EventUploadServiceAbstraction, @@ -35,11 +36,13 @@ export class InitService { private userAutoUnlockKeyService: UserAutoUnlockKeyService, private accountService: AccountService, private versionService: VersionService, + private sdkLoadService: SdkLoadService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { + await this.sdkLoadService.load(); await this.stateService.init(); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); @@ -49,7 +52,7 @@ export class InitService { await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id); } - setTimeout(() => this.notificationsService.init(), 3000); + this.notificationsService.startListening(); await this.vaultTimeoutService.init(true); await this.i18nService.init(); (this.eventUploadService as EventUploadService).init(true); diff --git a/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts b/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts index d407a709d4c..9358c4b200e 100644 --- a/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts +++ b/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts @@ -2,8 +2,14 @@ // @ts-strict-ignore import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FolderWithIdRequest } from "@bitwarden/common/src/vault/models/request/folder-with-id.request"; import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request"; diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index 4f2ae8f77e0..0101bc5aa97 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -7,8 +7,8 @@ import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-con import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request"; diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index ae47798420e..1acbc2012c5 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -8,7 +8,7 @@ import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractio import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; diff --git a/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts index 02910966d6e..8b9212373b9 100644 --- a/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts +++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts @@ -7,7 +7,7 @@ import { } from "@bitwarden/auth/common"; import { UserId } from "@bitwarden/common/types/guid"; import { BiometricsStatus } from "@bitwarden/key-management"; -import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular"; +import { LockComponentService, UnlockOptions } from "@bitwarden/key-management-ui"; export class WebLockComponentService implements LockComponentService { private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); diff --git a/apps/web/src/app/layouts/frontend-layout.component.html b/apps/web/src/app/layouts/frontend-layout.component.html index 72f0f1f1da3..d19af54f5df 100644 --- a/apps/web/src/app/layouts/frontend-layout.component.html +++ b/apps/web/src/app/layouts/frontend-layout.component.html @@ -1,6 +1,8 @@ -
+ +
- © {{ year }} Bitwarden Inc.
- {{ "versionNumber" | i18n: version }} -
+ +
© {{ year }} Bitwarden Inc.
+
{{ version }}
+ diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html index f34d32f5983..ef702c7e593 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html @@ -10,7 +10,7 @@ @@ -27,7 +27,7 @@ diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts index b09b32d060e..d64e1b817c1 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts @@ -3,11 +3,12 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, map, Observable } from "rxjs"; +import { combineLatest, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import type { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { DialogService, NavigationModule } from "@bitwarden/components"; @@ -20,12 +21,17 @@ import { TrialFlowService } from "./../../billing/services/trial-flow.service"; imports: [CommonModule, JslibModule, NavigationModule], }) export class OrgSwitcherComponent { - protected organizations$: Observable = - this.organizationService.organizations$.pipe( - map((orgs) => - orgs.filter((org) => this.filter(org)).sort((a, b) => a.name.localeCompare(b.name)), - ), - ); + protected organizations$: Observable = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService + .organizations$(account?.id) + .pipe( + map((orgs) => + orgs.filter((org) => this.filter(org)).sort((a, b) => a.name.localeCompare(b.name)), + ), + ), + ), + ); protected activeOrganization$: Observable = combineLatest([ this.route.paramMap, @@ -61,6 +67,7 @@ export class OrgSwitcherComponent { private organizationService: OrganizationService, private trialFlowService: TrialFlowService, protected billingApiService: BillingApiServiceAbstraction, + private accountService: AccountService, ) {} protected toggle(event?: MouseEvent) { diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts index 382ce8e026b..6e21c6c142a 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts @@ -7,6 +7,8 @@ import { BehaviorSubject } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { IconButtonModule, NavigationModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component"; import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service"; diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index 1c15f7cc7c1..181779c7c2e 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -1,16 +1,20 @@ import { Component, Directive, importProvidersFrom, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; import { LayoutComponent, NavigationModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; import { ProductSwitcherService } from "../shared/product-switcher.service"; @@ -22,11 +26,14 @@ import { NavigationProductSwitcherComponent } from "./navigation-switcher.compon }) class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); - organizations$ = MockOrganizationService._orgs; // eslint-disable-line rxjs/no-exposed-subjects + + organizations$(): Observable { + return MockOrganizationService._orgs.asObservable(); + } @Input() set mockOrgs(orgs: Organization[]) { - this.organizations$.next(orgs); + MockOrganizationService._orgs.next(orgs); } } @@ -52,6 +59,15 @@ class MockSyncService implements Partial { } } +class MockAccountService implements Partial { + activeAccount$?: Observable = of({ + id: "test-user-id" as UserId, + name: "Test User 1", + email: "test@email.com", + emailVerified: true, + }); +} + @Component({ selector: "story-layout", template: ``, @@ -86,6 +102,7 @@ export default { imports: [NavigationModule, RouterModule, LayoutComponent], providers: [ { provide: OrganizationService, useClass: MockOrganizationService }, + { provide: AccountService, useClass: MockAccountService }, { provide: ProviderService, useClass: MockProviderService }, { provide: SyncService, useClass: MockSyncService }, ProductSwitcherService, diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts index 80155166394..834571e2cb4 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts @@ -1,5 +1,7 @@ import { AfterViewInit, ChangeDetectorRef, Component, Input } from "@angular/core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { IconButtonType } from "@bitwarden/components/src/icon-button/icon-button.component"; import { ProductSwitcherService } from "./shared/product-switcher.service"; diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index 7a4df4bad00..44467bb2b29 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -1,16 +1,20 @@ import { Component, Directive, importProvidersFrom, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; import { IconButtonModule, LinkModule, MenuModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; import { ProductSwitcherContentComponent } from "./product-switcher-content.component"; @@ -22,11 +26,14 @@ import { ProductSwitcherService } from "./shared/product-switcher.service"; }) class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); - organizations$ = MockOrganizationService._orgs; // eslint-disable-line rxjs/no-exposed-subjects + + organizations$(): Observable { + return MockOrganizationService._orgs.asObservable(); + } @Input() set mockOrgs(orgs: Organization[]) { - this.organizations$.next(orgs); + MockOrganizationService._orgs.next(orgs); } } @@ -52,6 +59,15 @@ class MockSyncService implements Partial { } } +class MockAccountService implements Partial { + activeAccount$?: Observable = of({ + id: "test-user-id" as UserId, + name: "Test User 1", + email: "test@email.com", + emailVerified: true, + }); +} + @Component({ selector: "story-layout", template: ``, @@ -78,6 +94,8 @@ export default { ], imports: [JslibModule, MenuModule, IconButtonModule, LinkModule, RouterModule], providers: [ + { provide: AccountService, useClass: MockAccountService }, + MockAccountService, { provide: OrganizationService, useClass: MockOrganizationService }, MockOrganizationService, { provide: ProviderService, useClass: MockProviderService }, @@ -134,7 +152,9 @@ export default { ], } as Meta; -type Story = StoryObj; +type Story = StoryObj< + ProductSwitcherComponent & MockProviderService & MockOrganizationService & MockAccountService +>; const Template: Story = { render: (args) => ({ diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts index 919b3be0424..4187900060b 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts @@ -10,7 +10,11 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { ProductSwitcherService } from "./product-switcher.service"; @@ -19,8 +23,10 @@ describe("ProductSwitcherService", () => { let router: { url: string; events: Observable }; let organizationService: MockProxy; let providerService: MockProxy; + let accountService: FakeAccountService; let activeRouteParams = convertToParamMap({ organizationId: "1234" }); const getLastSync = jest.fn().mockResolvedValue(new Date("2024-05-14")); + const userId = Utils.newGuid() as UserId; // The service is dependent on the SyncService, which is behind a `setTimeout` // Most of the tests don't need to test this aspect so `advanceTimersByTime` @@ -36,10 +42,11 @@ describe("ProductSwitcherService", () => { router = mock(); organizationService = mock(); providerService = mock(); + accountService = mockAccountServiceWith(userId); router.url = "/"; router.events = of({}); - organizationService.organizations$ = of([{}] as Organization[]); + organizationService.organizations$.mockReturnValue(of([{}] as Organization[])); providerService.getAll.mockResolvedValue([] as Provider[]); TestBed.configureTestingModule({ @@ -47,6 +54,7 @@ describe("ProductSwitcherService", () => { { provide: Router, useValue: router }, { provide: OrganizationService, useValue: organizationService }, { provide: ProviderService, useValue: providerService }, + { provide: AccountService, useValue: accountService }, { provide: ActivatedRoute, useValue: { @@ -111,13 +119,15 @@ describe("ProductSwitcherService", () => { }); it("is included in bento when there is an organization with SM", async () => { - organizationService.organizations$ = of([ - { - id: "1234", - canAccessSecretsManager: true, - enabled: true, - }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { + id: "1234", + canAccessSecretsManager: true, + enabled: true, + }, + ] as Organization[]), + ); initiateService(); @@ -138,7 +148,9 @@ describe("ProductSwitcherService", () => { }); it("includes Admin Console in bento when a user has access to it", async () => { - organizationService.organizations$ = of([{ id: "1234", isOwner: true }] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([{ id: "1234", isOwner: true }] as Organization[]), + ); initiateService(); @@ -194,7 +206,9 @@ describe("ProductSwitcherService", () => { }); it("marks Admin Console as active", async () => { - organizationService.organizations$ = of([{ id: "1234", isOwner: true }] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([{ id: "1234", isOwner: true }] as Organization[]), + ); activeRouteParams = convertToParamMap({ organizationId: "1" }); router.url = "/organizations/"; @@ -225,20 +239,22 @@ describe("ProductSwitcherService", () => { it("updates secrets manager path when the org id is found in the path", async () => { router.url = "/sm/4243"; - organizationService.organizations$ = of([ - { - id: "23443234", - canAccessSecretsManager: true, - enabled: true, - name: "Org 2", - }, - { - id: "4243", - canAccessSecretsManager: true, - enabled: true, - name: "Org 32", - }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { + id: "23443234", + canAccessSecretsManager: true, + enabled: true, + name: "Org 2", + }, + { + id: "4243", + canAccessSecretsManager: true, + enabled: true, + name: "Org 32", + }, + ] as Organization[]), + ); initiateService(); @@ -253,10 +269,12 @@ describe("ProductSwitcherService", () => { it("updates admin console path when the org id is found in the path", async () => { router.url = "/organizations/111-22-33"; - organizationService.organizations$ = of([ - { id: "111-22-33", isOwner: true, name: "Test Org" }, - { id: "4243", isOwner: true, name: "My Org" }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { id: "111-22-33", isOwner: true, name: "Test Org" }, + { id: "4243", isOwner: true, name: "My Org" }, + ] as Organization[]), + ); initiateService(); diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts index 2c16886a2d4..f962879d61a 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts @@ -2,7 +2,16 @@ // @ts-strict-ignore import { Injectable } from "@angular/core"; import { ActivatedRoute, NavigationEnd, NavigationStart, ParamMap, Router } from "@angular/router"; -import { combineLatest, concatMap, filter, map, Observable, ReplaySubject, startWith } from "rxjs"; +import { + combineLatest, + concatMap, + filter, + map, + Observable, + ReplaySubject, + startWith, + switchMap, +} from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { @@ -11,6 +20,7 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SyncService } from "@bitwarden/common/platform/sync"; export type ProductSwitcherItem = { @@ -90,18 +100,20 @@ export class ProductSwitcherService { private router: Router, private i18n: I18nPipe, private syncService: SyncService, + private accountService: AccountService, ) { this.pollUntilSynced(); } + organizations$ = this.accountService.activeAccount$.pipe( + map((a) => a?.id), + switchMap((id) => this.organizationService.organizations$(id)), + ); + products$: Observable<{ bento: ProductSwitcherItem[]; other: ProductSwitcherItem[]; - }> = combineLatest([ - this.organizationService.organizations$, - this.route.paramMap, - this.triggerProductUpdate$, - ]).pipe( + }> = combineLatest([this.organizations$, this.route.paramMap, this.triggerProductUpdate$]).pipe( map(([orgs, ...rest]): [Organization[], ParamMap, void] => { return [ // Sort orgs by name to match the order within the sidebar diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index f0ac3ef9b48..e859993af32 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -31,7 +31,6 @@ import { WebLayoutModule } from "./web-layout.module"; }) export class UserLayoutComponent implements OnInit { protected readonly logo = PasswordManagerLogo; - isFreeFamilyFlagEnabled: boolean; protected hasFamilySponsorshipAvailable$: Observable; protected showSponsoredFamilies$: Observable; protected showSubscription$: Observable; diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 8a678a3b045..6863d6721e9 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from "@angular/core"; import { Route, RouterModule, Routes } from "@angular/router"; -import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component"; +import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { authGuard, @@ -9,7 +9,9 @@ import { redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, + activeAuthGuard, } from "@bitwarden/angular/auth/guards"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap"; import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; @@ -37,8 +39,11 @@ import { SsoComponent, VaultIcon, LoginDecryptionOptionsComponent, + NewDeviceVerificationComponent, + DeviceVerificationIcon, } from "@bitwarden/auth/angular"; -import { LockComponent } from "@bitwarden/key-management/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { LockComponent } from "@bitwarden/key-management-ui"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, @@ -67,8 +72,6 @@ import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emerg import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component"; import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module"; import { SsoComponentV1 } from "./auth/sso-v1.component"; -import { CompleteTrialInitiationComponent } from "./auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component"; -import { freeTrialTextResolver } from "./auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; import { TwoFactorAuthComponent } from "./auth/two-factor-auth.component"; import { TwoFactorComponent } from "./auth/two-factor.component"; import { UpdatePasswordComponent } from "./auth/update-password.component"; @@ -76,6 +79,8 @@ import { UpdateTempPasswordComponent } from "./auth/update-temp-password.compone import { VerifyEmailTokenComponent } from "./auth/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "./auth/verify-recover-delete.component"; import { SponsoredFamiliesComponent } from "./billing/settings/sponsored-families.component"; +import { CompleteTrialInitiationComponent } from "./billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component"; +import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component"; import { RouteDataProperties } from "./core"; import { FrontendLayoutComponent } from "./layouts/frontend-layout.component"; @@ -182,7 +187,7 @@ const routes: Routes = [ data: { pageIcon: DevicesIcon, pageTitle: { - key: "loginInitiated", + key: "logInRequestSent", }, pageSubtitle: { key: "aNotificationWasSentToYourDevice", @@ -538,12 +543,12 @@ const routes: Routes = [ } satisfies RouteDataProperties & AnonLayoutWrapperData, }, { - path: "2fa-timeout", + path: "authentication-timeout", canActivate: [unauthGuardFn()], children: [ { path: "", - component: TwoFactorTimeoutComponent, + component: AuthenticationTimeoutComponent, }, { path: "", @@ -580,6 +585,29 @@ const routes: Routes = [ titleId: "recoverAccountTwoStep", } satisfies RouteDataProperties & AnonLayoutWrapperData, }, + { + path: "device-verification", + canActivate: [ + canAccessFeature(FeatureFlag.NewDeviceVerification), + unauthGuardFn(), + activeAuthGuard(), + ], + children: [ + { + path: "", + component: NewDeviceVerificationComponent, + }, + ], + data: { + pageIcon: DeviceVerificationIcon, + pageTitle: { + key: "verifyIdentity", + }, + pageSubtitle: { + key: "weDontRecognizeThisDevice", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, { path: "accept-emergency", canActivate: [deepLinkGuard()], diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index 3f18440d231..0810a138de2 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -2,7 +2,7 @@ import { NgModule } from "@angular/core"; import { AuthModule } from "./auth"; import { LoginModule } from "./auth/login/login.module"; -import { TrialInitiationModule } from "./auth/trial-initiation/trial-initiation.module"; +import { TrialInitiationModule } from "./billing/trial-initiation/trial-initiation.module"; import { LooseComponentsModule, SharedModule } from "./shared"; import { AccessComponent } from "./tools/send/access.component"; import { OrganizationBadgeModule } from "./vault/individual-vault/organization-badge/organization-badge.module"; diff --git a/apps/web/src/app/platform/notifications/permissions-webpush-connection.service.ts b/apps/web/src/app/platform/notifications/permissions-webpush-connection.service.ts new file mode 100644 index 00000000000..44866285251 --- /dev/null +++ b/apps/web/src/app/platform/notifications/permissions-webpush-connection.service.ts @@ -0,0 +1,54 @@ +import { concat, defer, fromEvent, map, Observable, of, switchMap } from "rxjs"; + +import { SupportStatus } from "@bitwarden/common/platform/misc/support-status"; +// eslint-disable-next-line no-restricted-imports -- In platform owned code. +import { + WebPushConnector, + WorkerWebPushConnectionService, +} from "@bitwarden/common/platform/notifications/internal"; +import { UserId } from "@bitwarden/common/types/guid"; + +export class PermissionsWebPushConnectionService extends WorkerWebPushConnectionService { + override supportStatus$(userId: UserId): Observable> { + return this.notificationPermission$().pipe( + switchMap((notificationPermission) => { + if (notificationPermission === "denied") { + return of>({ + type: "not-supported", + reason: "permission-denied", + }); + } + + if (notificationPermission === "default") { + return of>({ + type: "needs-configuration", + reason: "permission-not-requested", + }); + } + + if (notificationPermission === "prompt") { + return of>({ + type: "needs-configuration", + reason: "prompt-must-be-granted", + }); + } + + // Delegate to default worker checks + return super.supportStatus$(userId); + }), + ); + } + + private notificationPermission$() { + return concat( + of(Notification.permission), + defer(async () => { + return await window.navigator.permissions.query({ name: "notifications" }); + }).pipe( + switchMap((permissionStatus) => { + return fromEvent(permissionStatus, "change").pipe(map(() => permissionStatus.state)); + }), + ), + ); + } +} diff --git a/apps/web/src/app/platform/web-sdk-client-factory.ts b/apps/web/src/app/platform/web-sdk-client-factory.ts deleted file mode 100644 index 0dd43ecbb92..00000000000 --- a/apps/web/src/app/platform/web-sdk-client-factory.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; -import * as sdk from "@bitwarden/sdk-internal"; - -/** - * SDK client factory with a js fallback for when WASM is not supported. - */ -export class WebSdkClientFactory implements SdkClientFactory { - async createSdkClient( - ...args: ConstructorParameters - ): Promise { - const module = await load(); - - (sdk as any).init(module); - - return Promise.resolve(new sdk.BitwardenClient(...args)); - } -} - -// https://stackoverflow.com/a/47880734 -const supported = (() => { - try { - if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { - const module = new WebAssembly.Module( - Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00), - ); - if (module instanceof WebAssembly.Module) { - return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; - } - } - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - // ignore - } - return false; -})(); - -async function load() { - if (supported) { - return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); - } else { - return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"); - } -} diff --git a/apps/web/src/app/platform/web-sdk-load.service.ts b/apps/web/src/app/platform/web-sdk-load.service.ts new file mode 100644 index 00000000000..cae3399b81e --- /dev/null +++ b/apps/web/src/app/platform/web-sdk-load.service.ts @@ -0,0 +1,31 @@ +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import * as sdk from "@bitwarden/sdk-internal"; + +// https://stackoverflow.com/a/47880734 +const supported = (() => { + try { + if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { + const module = new WebAssembly.Module( + Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00), + ); + if (module instanceof WebAssembly.Module) { + return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; + } + } + } catch { + // ignore + } + return false; +})(); + +export class WebSdkLoadService implements SdkLoadService { + async load(): Promise { + let module: any; + if (supported) { + module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); + } else { + module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"); + } + (sdk as any).init(module); + } +} diff --git a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts index 805745c369d..5edd8bc046e 100644 --- a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts +++ b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Guid } from "@bitwarden/common/src/types/guid"; +import { Guid } from "@bitwarden/common/types/guid"; export class RequestSMAccessRequest { OrganizationId: Guid; diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts index 8909df6cf8a..d1ab7689cfe 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts @@ -3,9 +3,12 @@ import { Component, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Guid } from "@bitwarden/common/types/guid"; import { NoItemsModule, SearchModule, ToastService } from "@bitwarden/components"; @@ -39,10 +42,12 @@ export class RequestSMAccessComponent implements OnInit { private organizationService: OrganizationService, private smLandingApiService: SmLandingApiService, private toastService: ToastService, + private accountService: AccountService, ) {} async ngOnInit() { - this.organizations = (await this.organizationService.getAll()) + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organizations = (await firstValueFrom(this.organizationService.organizations$(userId))) .filter((e) => e.enabled) .sort((a, b) => a.name.localeCompare(b.name)); diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts index 3698031a5b6..4d9dceab34a 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts @@ -1,9 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { NoItemsModule, SearchModule } from "@bitwarden/components"; import { HeaderModule } from "../../layouts/header/header.module"; @@ -22,10 +25,16 @@ export class SMLandingComponent implements OnInit { showSecretsManagerInformation: boolean = true; showGiveMembersAccessInstructions: boolean = false; - constructor(private organizationService: OrganizationService) {} + constructor( + private organizationService: OrganizationService, + private accountService: AccountService, + ) {} async ngOnInit() { - const enabledOrganizations = (await this.organizationService.getAll()).filter((e) => e.enabled); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const enabledOrganizations = ( + await firstValueFrom(this.organizationService.organizations$(userId)) + ).filter((e) => e.enabled); if (enabledOrganizations.length > 0) { this.handleEnabledOrganizations(enabledOrganizations); diff --git a/apps/web/src/app/settings/domain-rules.component.html b/apps/web/src/app/settings/domain-rules.component.html index a3bea63fb86..7e9e7d6ed64 100644 --- a/apps/web/src/app/settings/domain-rules.component.html +++ b/apps/web/src/app/settings/domain-rules.component.html @@ -43,7 +43,7 @@ -

{{ "globalEqDomains" | i18n }}

+

{{ "globalEqDomains" | i18n }}

this.organizationService.organizations$(account?.id)), + ); this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => { this.organizations = orgs; }); diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index 07dc218bd64..16541bdc109 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -7,7 +7,11 @@ import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("ExposedPasswordsReportComponent", () => { let auditService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { syncServiceMock = mock(); auditService = mock(); organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); + accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -44,6 +51,10 @@ describe("ExposedPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts index 13d2804c5e5..1a0d4043b74 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -25,6 +26,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple protected cipherService: CipherService, protected auditService: AuditService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -35,6 +37,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts index 80760eb5dec..385bda03f28 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts @@ -6,8 +6,12 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -20,11 +24,14 @@ describe("InactiveTwoFactorReportComponent", () => { let fixture: ComponentFixture; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); + accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -38,6 +45,10 @@ describe("InactiveTwoFactorReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts index 792ad0616f2..52c52041c9d 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -27,6 +28,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, private logService: LogService, passwordRepromptService: PasswordRepromptService, @@ -38,6 +40,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 9d16bbb1c62..6a26cd24fe5 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -6,7 +6,11 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -19,11 +23,15 @@ describe("ReusedPasswordsReportComponent", () => { let fixture: ComponentFixture; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); + accountService = mockAccountServiceWith(userId); + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -37,6 +45,10 @@ describe("ReusedPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts index a8806acea13..a5c1c65560b 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -24,6 +25,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -34,6 +36,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts index 5f66814fdf1..7cd159108b8 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts @@ -7,7 +7,11 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("UnsecuredWebsitesReportComponent", () => { let organizationService: MockProxy; let syncServiceMock: MockProxy; let collectionService: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); collectionService = mock(); + accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -40,6 +47,10 @@ describe("UnsecuredWebsitesReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts index 6a1ba1f6333..350e5c03980 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { CollectionService, Collection } from "@bitwarden/admin-console/common"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -22,6 +23,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -33,6 +35,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index bcace60ac0c..578c220f396 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -6,8 +6,12 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("WeakPasswordsReportComponent", () => { let passwordStrengthService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { syncServiceMock = mock(); passwordStrengthService = mock(); organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); + accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -44,6 +51,10 @@ describe("WeakPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts index f3ad6840c8b..c374ecd0e4a 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -32,6 +33,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen protected cipherService: CipherService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected organizationService: OrganizationService, + protected accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -42,6 +44,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/reports-routing.module.ts b/apps/web/src/app/tools/reports/reports-routing.module.ts index cad6586bb82..941e6eb7d3d 100644 --- a/apps/web/src/app/tools/reports/reports-routing.module.ts +++ b/apps/web/src/app/tools/reports/reports-routing.module.ts @@ -3,7 +3,7 @@ import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; -import { hasPremiumGuard } from "../../core/guards/has-premium.guard"; +import { hasPremiumGuard } from "../../billing/guards/has-premium.guard"; import { BreachReportComponent } from "./pages/breach-report.component"; import { ExposedPasswordsReportComponent } from "./pages/exposed-passwords-report.component"; diff --git a/apps/web/src/app/tools/send/send-access-file.component.ts b/apps/web/src/app/tools/send/send-access-file.component.ts index b55e955f355..05408bc34f7 100644 --- a/apps/web/src/app/tools/send/send-access-file.component.ts +++ b/apps/web/src/app/tools/send/send-access-file.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Component, Input } from "@angular/core"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html index fbe0649c7aa..61fc290f6fe 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html @@ -70,11 +70,11 @@ - {{ "grantAddAccessCollectionWarning" | i18n }} + {{ "grantManageCollectionWarning" | i18n }} {{ "grantCollectionAccess" | i18n }} {{ diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 6141e983a68..a035202516a 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -5,6 +5,7 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angula import { AbstractControl, FormBuilder, Validators } from "@angular/forms"; import { combineLatest, + firstValueFrom, map, Observable, of, @@ -24,8 +25,13 @@ import { CollectionResponse, CollectionView, } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -110,6 +116,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private organizationUserApiService: OrganizationUserApiService, private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, + private accountService: AccountService, private toastService: ToastService, ) { this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info; @@ -122,7 +129,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.formGroup.controls.selectedOrg.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe((id) => this.loadOrg(id)); - this.organizations$ = this.organizationService.organizations$.pipe( + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organizations$ = this.organizationService.organizations$(userId).pipe( first(), map((orgs) => orgs @@ -140,8 +150,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { } async loadOrg(orgId: string) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const organization$ = this.organizationService - .get$(orgId) + .organizations$(userId) + .pipe(getOrganizationById(orgId)) .pipe(shareReplay({ refCount: true, bufferSize: 1 })); const groups$ = organization$.pipe( switchMap((organization) => { diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html index c19329f7121..45df670570d 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html @@ -39,7 +39,7 @@ - + - +

diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index a530fd0cc85..0af0d720b0e 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -4,7 +4,7 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, Observable, Subject, switchMap } from "rxjs"; +import { firstValueFrom, Subject, switchMap } from "rxjs"; import { map } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; @@ -36,6 +36,7 @@ import { } from "@bitwarden/components"; import { CipherAttachmentsComponent, + CipherFormComponent, CipherFormConfig, CipherFormGenerationService, CipherFormModule, @@ -144,6 +145,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { @ViewChild("dialogContent") protected dialogContent: ElementRef; + @ViewChild(CipherFormComponent) cipherFormComponent!: CipherFormComponent; + /** * Tracks if the cipher was ever modified while the dialog was open. Used to ensure the dialog emits the correct result * in case of closing with the X button or ESC key. @@ -204,16 +207,24 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { ), ); + protected get isTrashFilter() { + return this.filter?.type === "trash"; + } + + protected get showCancel() { + return !this.isTrashFilter && !this.showCipherView; + } + + protected get showClose() { + return this.isTrashFilter && !this.showRestore; + } + /** * Determines if the user may restore the item. * A user may restore items if they have delete permissions and the item is in the trash. */ protected async canUserRestore() { - return ( - this.filter?.type === "trash" && - this.cipher?.isDeleted && - (await firstValueFrom(this.canDeleteCipher$)) - ); + return this.isTrashFilter && this.cipher?.isDeleted && this.canDelete; } protected showRestore: boolean; @@ -226,8 +237,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { return this.params.disableForm; } - protected get canDelete() { - return this.cipher?.edit ?? false; + protected get showEdit() { + return this.showCipherView && !this.isTrashFilter && !this.showRestore; } protected get showDelete() { @@ -255,10 +266,10 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { protected formConfig: CipherFormConfig = this.params.formConfig; - protected canDeleteCipher$: Observable; - protected filter: RoutedVaultFilterModel; + protected canDelete = false; + constructor( @Inject(DIALOG_DATA) protected params: VaultItemDialogParams, private dialogRef: DialogRef, @@ -299,10 +310,12 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { (o) => o.id === this.cipher.organizationId, ); - this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$( - this.cipher, - [this.params.activeCollectionId], - this.params.isAdminConsoleAction, + this.canDelete = await firstValueFrom( + this.cipherAuthorizationService.canDeleteCipher$( + this.cipher, + [this.params.activeCollectionId], + this.params.isAdminConsoleAction, + ), ); await this.eventCollectionService.collect( @@ -432,6 +445,22 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { result.action === AttachmentDialogResult.Removed || result.action === AttachmentDialogResult.Uploaded ) { + const updatedCipher = await this.cipherService.get(this.formConfig.originalCipher?.id); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const updatedCipherView = await updatedCipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), + ); + + this.cipherFormComponent.patchCipher((currentCipher) => { + currentCipher.attachments = updatedCipherView.attachments; + currentCipher.revisionDate = updatedCipherView.revisionDate; + + return currentCipher; + }); + this._cipherModified = true; } }; diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts index 556d3d54594..4af08e19d74 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts @@ -42,6 +42,7 @@ export class VaultCipherRowComponent implements OnInit { @Input() collections: CollectionView[]; @Input() viewingOrgVault: boolean; @Input() canEditCipher: boolean; + @Input() canAssignCollections: boolean; @Input() canManageCollection: boolean; @Output() onEvent = new EventEmitter(); @@ -52,11 +53,11 @@ export class VaultCipherRowComponent implements OnInit { protected CipherType = CipherType; private permissionList = getPermissionList(); private permissionPriority = [ - "canManage", - "canEdit", - "canEditExceptPass", - "canView", - "canViewExceptPass", + "manageCollection", + "editItems", + "editItemsHidePass", + "viewItems", + "viewItemsHidePass", ]; protected organization?: Organization; @@ -101,7 +102,7 @@ export class VaultCipherRowComponent implements OnInit { } protected get showAssignToCollections() { - return this.organizations?.length && this.canEditCipher && !this.cipher.isDeleted; + return this.organizations?.length && this.canAssignCollections && !this.cipher.isDeleted; } protected get showClone() { @@ -118,7 +119,7 @@ export class VaultCipherRowComponent implements OnInit { protected get permissionText() { if (!this.cipher.organizationId || this.cipher.collectionIds.length === 0) { - return this.i18nService.t("canManage"); + return this.i18nService.t("manageCollection"); } const filteredCollections = this.collections.filter((collection) => { @@ -208,6 +209,6 @@ export class VaultCipherRowComponent implements OnInit { return true; // Always show checkbox in individual vault or for non-org items } - return this.organization.canEditAllCiphers || this.cipher.edit; + return this.organization.canEditAllCiphers || (this.cipher.edit && this.cipher.viewPassword); } } diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 404e26fc210..d07ba46d136 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -74,7 +74,7 @@ export class VaultCollectionRowComponent { get permissionText() { if (this.collection.id == Unassigned && this.organization?.canEditUnassignedCiphers) { - return this.i18nService.t("canEdit"); + return this.i18nService.t("editItems"); } if ((this.collection as CollectionAdminView).assigned) { const permissionList = getPermissionList(); diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index 653d05ef129..a32def5fc0c 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -144,6 +144,7 @@ [collections]="allCollections" [checked]="selection.isSelected(item)" [canEditCipher]="canEditCipher(item.cipher)" + [canAssignCollections]="canAssignCollections(item.cipher)" [canManageCollection]="canManageCollection(item.cipher)" (checkedToggled)="selection.toggle(item)" (onEvent)="event($event)" diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 3e1cf173a47..a641c5b5908 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -236,6 +236,13 @@ export class VaultItemsComponent { return (organization.canEditAllCiphers && this.viewingOrgVault) || cipher.edit; } + protected canAssignCollections(cipher: CipherView) { + const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId); + return ( + (organization?.canEditAllCiphers && this.viewingOrgVault) || cipher.canAssignToCollections + ); + } + protected canManageCollection(cipher: CipherView) { // If the cipher is not part of an organization (personal item), user can manage it if (cipher.organizationId == null) { @@ -461,7 +468,7 @@ export class VaultItemsComponent { private allCiphersHaveEditAccess(): boolean { return this.selection.selected .filter(({ cipher }) => cipher) - .every(({ cipher }) => cipher?.edit); + .every(({ cipher }) => cipher?.edit && cipher?.viewPassword); } private getUniqueOrganizationIds(): Set { diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts index 1ca9b0de47c..9127a213a45 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts @@ -49,7 +49,7 @@ describe("AddEditComponentV2", () => { } as Organization; organizationService = mock(); - organizationService.organizations$ = of([mockOrganization]); + organizationService.organizations$.mockReturnValue(of([mockOrganization])); policyService = mock(); policyService.policyAppliesToActiveUser$.mockImplementation((policyType: PolicyType) => diff --git a/apps/web/src/app/vault/individual-vault/attachments.component.ts b/apps/web/src/app/vault/individual-vault/attachments.component.ts index a6c25b71fd4..c6079dbe78f 100644 --- a/apps/web/src/app/vault/individual-vault/attachments.component.ts +++ b/apps/web/src/app/vault/individual-vault/attachments.component.ts @@ -4,7 +4,7 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/ang import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts index 1dc15a7471a..2deb5d35341 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts @@ -8,6 +8,7 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Checkable, isChecked } from "@bitwarden/common/types/checkable"; @@ -76,7 +77,8 @@ export class BulkShareDialogComponent implements OnInit, OnDestroy { this.nonShareableCount = this.ciphers.length - this.shareableCiphers.length; const allCollections = await this.collectionService.getAllDecrypted(); this.writeableCollections = allCollections.filter((c) => !c.readOnly); - this.organizations = await this.organizationService.getAll(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organizations = await firstValueFrom(this.organizationService.organizations$(userId)); if (this.organizationId == null && this.organizations.length > 0) { this.organizationId = this.organizations[0].id; } diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index 390b95fa2b1..475cfc2df22 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -204,6 +204,7 @@ export class VaultBannersService { private async isLowKdfIteration(userId: UserId) { const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId)); return ( + kdfConfig != null && kdfConfig.kdfType === KdfType.PBKDF2_SHA256 && kdfConfig.iterations < PBKDF2KdfConfig.ITERATIONS.defaultValue ); diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts index 161b2ccb7ef..5a0c0a535b4 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts @@ -9,7 +9,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { BannerModule } from "@bitwarden/components"; import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component"; -import { FreeTrial } from "../../../core/types/free-trial"; +import { FreeTrial } from "../../../billing/types/free-trial"; import { SharedModule } from "../../../shared"; import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service"; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 6255ee11c49..37e3aca6cd5 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -1,7 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; -import { combineLatest, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs"; +import { + combineLatest, + firstValueFrom, + map, + Observable, + of, + Subject, + switchMap, + takeUntil, +} from "rxjs"; import { OrganizationUserApiService, @@ -15,7 +24,9 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -60,6 +71,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { private toastService: ToastService, private configService: ConfigService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -67,16 +79,19 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)), ); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const managingOrg$ = this.configService .getFeatureFlag$(FeatureFlag.AccountDeprovisioning) .pipe( switchMap((isAccountDeprovisioningEnabled) => isAccountDeprovisioningEnabled - ? this.organizationService.organizations$.pipe( - map((organizations) => - organizations.find((o) => o.userIsManagedByOrganization === true), - ), - ) + ? this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => + organizations.find((o) => o.userIsManagedByOrganization === true), + ), + ) : of(null), ), ); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts index b4f52180e52..b8494c8aa54 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts @@ -4,8 +4,8 @@ import { Observable } from "rxjs"; import { CollectionAdminView, CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherTypeFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts index 47003d51cae..8d74f69ed06 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts @@ -62,7 +62,7 @@ describe("vault filter service", () => { personalOwnershipPolicy = new ReplaySubject(1); singleOrgPolicy = new ReplaySubject(1); - organizationService.memberOrganizations$ = organizations; + organizationService.memberOrganizations$.mockReturnValue(organizations); folderService.folderViews$.mockReturnValue(folderViews); collectionService.decryptedCollections$ = collectionViews; policyService.policyAppliesToActiveUser$ diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index 97b44132e60..03dfa92d0b5 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -48,8 +48,12 @@ const NestingDelimiter = "/"; export class VaultFilterService implements VaultFilterServiceAbstraction { private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + memberOrganizations$ = this.activeUserId$.pipe( + switchMap((id) => this.organizationService.memberOrganizations$(id)), + ); + organizationTree$: Observable> = combineLatest([ - this.organizationService.memberOrganizations$, + this.memberOrganizations$, this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg), this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), ]).pipe( @@ -270,6 +274,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { folderCopy.id = f.id; folderCopy.revisionDate = f.revisionDate; folderCopy.icon = "bwi-folder"; + folderCopy.fullName = f.name; // save full folder name before separating it into parts const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : []; ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter); }); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts index 0cd385bd19d..9259dd08114 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts @@ -1,8 +1,8 @@ import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; export type CipherStatus = "all" | "favorites" | "trash" | CipherType; @@ -10,5 +10,13 @@ export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: str export type CollectionFilter = CollectionAdminView & { icon: string; }; -export type FolderFilter = FolderView & { icon: string }; +export type FolderFilter = FolderView & { + icon: string; + /** + * Full folder name. + * + * Used for when the folder `name` property is be separated into parts. + */ + fullName?: string; +}; export type OrganizationFilter = Organization & { icon: string; hideOptions?: boolean }; diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 46c678fd987..950c1d77731 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -47,7 +47,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; 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 { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; @@ -74,6 +77,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { DialogService, Icons, ToastService } from "@bitwarden/components"; import { + AddEditFolderDialogComponent, + AddEditFolderDialogResult, CipherFormConfig, CollectionAssignmentResult, DecryptionFailureDialogComponent, @@ -82,7 +87,7 @@ import { } from "@bitwarden/vault"; import { TrialFlowService } from "../../billing/services/trial-flow.service"; -import { FreeTrial } from "../../core/types/free-trial"; +import { FreeTrial } from "../../billing/types/free-trial"; import { SharedModule } from "../../shared/shared.module"; import { AssignCollectionsWebComponent } from "../components/assign-collections"; import { @@ -115,7 +120,6 @@ import { BulkMoveDialogResult, openBulkMoveDialog, } from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component"; -import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component"; import { VaultBannersComponent } from "./vault-banners/vault-banners.component"; import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component"; import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service"; @@ -193,7 +197,11 @@ export class VaultComponent implements OnInit, OnDestroy { private hasSubscription$ = new BehaviorSubject(false); private vaultItemDialogRef?: DialogRef | undefined; - private readonly unpaidSubscriptionDialog$ = this.organizationService.organizations$.pipe( + private organizations$ = this.accountService.activeAccount$ + .pipe(map((a) => a?.id)) + .pipe(switchMap((id) => this.organizationService.organizations$(id))); + + private readonly unpaidSubscriptionDialog$ = this.organizations$.pipe( filter((organizations) => organizations.length === 1), map(([organization]) => organization), switchMap((organization) => @@ -212,9 +220,8 @@ export class VaultComponent implements OnInit, OnDestroy { ), ), ); - protected organizationsPaymentStatus$: Observable = combineLatest([ - this.organizationService.organizations$.pipe( + this.organizations$.pipe( map( (organizations) => organizations?.filter((org) => org.isOwner && org.canViewBillingHistory) ?? [], @@ -501,7 +508,7 @@ export class VaultComponent implements OnInit, OnDestroy { filter$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(this.activeUserId), allCollections$, - this.organizationService.organizations$, + this.organizations$, ciphers$, collections$, selectedCollection$, @@ -601,20 +608,24 @@ export class VaultComponent implements OnInit, OnDestroy { await this.filterComponent.filters?.organizationFilter?.action(orgNode); } - addFolder = async (): Promise => { - openFolderAddEditDialog(this.dialogService); + addFolder = (): void => { + AddEditFolderDialogComponent.open(this.dialogService); }; editFolder = async (folder: FolderFilter): Promise => { - const dialog = openFolderAddEditDialog(this.dialogService, { - data: { - folderId: folder.id, + const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, { + editFolderConfig: { + // Shallow copy is used so the original folder object is not modified + folder: { + ...folder, + name: folder.fullName ?? folder.name, // If the filter has a fullName populated, use that as the editable name + }, }, }); - const result = await lastValueFrom(dialog.closed); + const result = await lastValueFrom(dialogRef.closed); - if (result === FolderAddEditDialogResult.Deleted) { + if (result === AddEditFolderDialogResult.Deleted) { await this.router.navigate([], { queryParams: { folderId: null }, queryParamsHandling: "merge", @@ -646,7 +657,9 @@ export class VaultComponent implements OnInit, OnDestroy { this.messagingService.send("premiumRequired"); return; } else if (cipher.organizationId != null) { - const org = await this.organizationService.get(cipher.organizationId); + const org = await firstValueFrom( + this.organizations$.pipe(getOrganizationById(cipher.organizationId)), + ); if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) { this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId, @@ -971,7 +984,9 @@ export class VaultComponent implements OnInit, OnDestroy { } async deleteCollection(collection: CollectionView): Promise { - const organization = await this.organizationService.get(collection.organizationId); + const organization = await firstValueFrom( + this.organizations$.pipe(getOrganizationById(collection.organizationId)), + ); if (!collection.canDelete(organization)) { this.showMissingPermissionsError(); return; @@ -1068,7 +1083,7 @@ export class VaultComponent implements OnInit, OnDestroy { } } - async restore(c: CipherView): Promise { + restore = async (c: CipherView): Promise => { if (!c.isDeleted) { return; } @@ -1093,7 +1108,7 @@ export class VaultComponent implements OnInit, OnDestroy { } catch (e) { this.logService.error(e); } - } + }; async bulkRestore(ciphers: CipherView[]) { if (ciphers.some((c) => !c.edit)) { @@ -1136,9 +1151,7 @@ export class VaultComponent implements OnInit, OnDestroy { .filter((i) => i.cipher === undefined) .map((i) => i.collection.organizationId); const orgs = await firstValueFrom( - this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => orgIds.includes(o.id))), - ), + this.organizations$.pipe(map((orgs) => orgs.filter((o) => orgIds.includes(o.id)))), ); await this.bulkDelete(ciphers, collections, orgs); } diff --git a/apps/web/src/app/vault/individual-vault/view.component.spec.ts b/apps/web/src/app/vault/individual-vault/view.component.spec.ts index bde9f564c4a..9bea7f14eb5 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.spec.ts @@ -1,6 +1,7 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -11,7 +12,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -41,6 +43,8 @@ describe("ViewComponent", () => { const mockParams: ViewCipherDialogParams = { cipher: mockCipher, }; + const userId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(userId); beforeEach(async () => { await TestBed.configureTestingModule({ @@ -53,10 +57,14 @@ describe("ViewComponent", () => { { provide: CipherService, useValue: mock() }, { provide: ToastService, useValue: mock() }, { provide: MessagingService, useValue: mock() }, + { + provide: AccountService, + useValue: accountService, + }, { provide: LogService, useValue: mock() }, { provide: OrganizationService, - useValue: { get: jest.fn().mockResolvedValue(mockOrganization) }, + useValue: { organizations$: jest.fn().mockReturnValue(of([mockOrganization])) }, }, { provide: CollectionService, useValue: mock() }, { provide: FolderService, useValue: mock() }, diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index e9ca2bf8f8c..9b6d15c581d 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -3,16 +3,19 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Inject, OnInit } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, firstValueFrom, map } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { CollectionId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -23,9 +26,8 @@ import { DialogService, ToastService, } from "@bitwarden/components"; +import { CipherViewComponent } from "@bitwarden/vault"; -import { PremiumUpgradePromptService } from "../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; -import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component"; import { SharedModule } from "../../shared/shared.module"; import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service"; import { WebViewPasswordHistoryService } from "../services/web-view-password-history.service"; @@ -94,6 +96,7 @@ export class ViewComponent implements OnInit { private toastService: ToastService, private organizationService: OrganizationService, private cipherAuthorizationService: CipherAuthorizationService, + private accountService: AccountService, ) {} /** @@ -103,8 +106,17 @@ export class ViewComponent implements OnInit { this.cipher = this.params.cipher; this.collections = this.params.collections; this.cipherTypeString = this.getCipherViewTypeString(); + + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (this.cipher.organizationId) { - this.organization = await this.organizationService.get(this.cipher.organizationId); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => organizations.find((o) => o.id === this.cipher.organizationId)), + ), + ); } this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index c8badffb36f..37136a86cdb 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -6,7 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; diff --git a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts index d9ba8af49fa..4058c1151fb 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -10,8 +10,12 @@ import { OrganizationUserApiService, CollectionView, } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -63,6 +67,7 @@ export class BulkCollectionsDialogComponent implements OnDestroy { private dialogRef: DialogRef, private formBuilder: FormBuilder, private organizationService: OrganizationService, + private accountService: AccountService, private groupService: GroupApiService, private organizationUserApiService: OrganizationUserApiService, private platformUtilsService: PlatformUtilsService, @@ -71,7 +76,13 @@ export class BulkCollectionsDialogComponent implements OnDestroy { private toastService: ToastService, ) { this.numCollections = this.params.collections.length; - const organization$ = this.organizationService.get$(this.params.organizationId); + const organization$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService + .organizations$(account?.id) + .pipe(getOrganizationById(this.params.organizationId)), + ), + ); const groups$ = organization$.pipe( switchMap((organization) => { if (!organization.useGroups) { diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts index 25976c4fb82..5ccddeee4bb 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts @@ -7,7 +7,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; @@ -50,8 +51,7 @@ describe("AdminConsoleCipherFormConfigService", () => { readOnly: false, } as CollectionAdminView; - const organization$ = new BehaviorSubject(testOrg as Organization); - const organizations$ = new BehaviorSubject([testOrg, testOrg2] as Organization[]); + const orgs$ = new BehaviorSubject([testOrg, testOrg2] as Organization[]); const getCipherAdmin = jest.fn().mockResolvedValue(null); const getCipher = jest.fn().mockResolvedValue(null); @@ -65,7 +65,7 @@ describe("AdminConsoleCipherFormConfigService", () => { TestBed.configureTestingModule({ providers: [ AdminConsoleCipherFormConfigService, - { provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } }, + { provide: OrganizationService, useValue: { organizations$: () => orgs$ } }, { provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([collection, collection2]) }, @@ -80,6 +80,17 @@ describe("AdminConsoleCipherFormConfigService", () => { }, { provide: ApiService, useValue: { getCipherAdmin } }, { provide: CipherService, useValue: { get: getCipher } }, + { + provide: AccountService, + useValue: { + activeAccount$: new BehaviorSubject({ + id: "123-456-789" as UserId, + email: "test@email.com", + emailVerified: true, + name: "Test User", + }), + }, + }, ], }); adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts index 0d3db55d3d6..32e75644d09 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts @@ -9,17 +9,14 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherFormConfig, CipherFormConfigService, CipherFormMode } from "@bitwarden/vault"; -import { - CipherFormConfig, - CipherFormConfigService, - CipherFormMode, -} from "../../../../../../../libs/vault/src/cipher-form/abstractions/cipher-form-config.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; /** Admin Console implementation of the `CipherFormConfigService`. */ @@ -31,6 +28,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ private collectionAdminService: CollectionAdminService = inject(CollectionAdminService); private cipherService: CipherService = inject(CipherService); private apiService: ApiService = inject(ApiService); + private accountService: AccountService = inject(AccountService); private allowPersonalOwnership$ = this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) @@ -41,12 +39,16 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ filter((filter) => filter !== undefined), ); - private allOrganizations$ = this.organizationService.organizations$.pipe( - map((orgs) => { - return orgs.filter( - (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, - ); - }), + private allOrganizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService.organizations$(account?.id).pipe( + map((orgs) => { + return orgs.filter( + (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, + ); + }), + ), + ), ); private organization$ = combineLatest([this.allOrganizations$, this.organizationId$]).pipe( diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index d28148c49dc..7f118a48db3 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -11,7 +11,6 @@ import { Unassigned, } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -90,7 +89,6 @@ export class VaultHeaderComponent implements OnInit { @Output() searchTextChanged = new EventEmitter(); protected CollectionDialogTabType = CollectionDialogTabType; - protected organizations$ = this.organizationService.organizations$; /** * Whether the extension refresh feature flag is enabled. @@ -101,7 +99,6 @@ export class VaultHeaderComponent implements OnInit { protected CipherType = CipherType; constructor( - private organizationService: OrganizationService, private i18nService: I18nService, private dialogService: DialogService, private collectionAdminService: CollectionAdminService, diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 779266d830f..fe76f9842e9 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -49,6 +49,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; 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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; @@ -91,7 +92,7 @@ import { ResellerWarningService, } from "../../billing/services/reseller-warning.service"; import { TrialFlowService } from "../../billing/services/trial-flow.service"; -import { FreeTrial } from "../../core/types/free-trial"; +import { FreeTrial } from "../../billing/types/free-trial"; import { SharedModule } from "../../shared"; 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"; @@ -213,19 +214,24 @@ export class VaultComponent implements OnInit, OnDestroy { private resellerManagedOrgAlert: boolean; private vaultItemDialogRef?: DialogRef | undefined; - private readonly unpaidSubscriptionDialog$ = this.organizationService.organizations$.pipe( - filter((organizations) => organizations.length === 1), - map(([organization]) => organization), - switchMap((organization) => - from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe( - tap((organizationMetaData) => { - this.hasSubscription$.next(organizationMetaData.hasSubscription); - }), - switchMap((organizationMetaData) => - from( - this.trialFlowService.handleUnpaidSubscriptionDialog( - organization, - organizationMetaData, + private readonly unpaidSubscriptionDialog$ = this.accountService.activeAccount$.pipe( + map((account) => account?.id), + switchMap((id) => + this.organizationService.organizations$(id).pipe( + filter((organizations) => organizations.length === 1), + map(([organization]) => organization), + switchMap((organization) => + from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe( + tap((organizationMetaData) => { + this.hasSubscription$.next(organizationMetaData.hasSubscription); + }), + switchMap((organizationMetaData) => + from( + this.trialFlowService.handleUnpaidSubscriptionDialog( + organization, + organizationMetaData, + ), + ), ), ), ), @@ -268,6 +274,7 @@ export class VaultComponent implements OnInit, OnDestroy { protected billingApiService: BillingApiServiceAbstraction, private organizationBillingService: OrganizationBillingServiceAbstraction, private resellerWarningService: ResellerWarningService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -292,10 +299,19 @@ export class VaultComponent implements OnInit, OnDestroy { distinctUntilChanged(), ); - const organization$ = organizationId$.pipe( - switchMap((organizationId) => this.organizationService.get$(organizationId)), - takeUntil(this.destroy$), - shareReplay({ refCount: false, bufferSize: 1 }), + const organization$ = this.accountService.activeAccount$.pipe( + map((account) => account?.id), + switchMap((id) => + organizationId$.pipe( + switchMap((organizationId) => + this.organizationService + .organizations$(id) + .pipe(map((organizations) => organizations.find((org) => org.id === organizationId))), + ), + takeUntil(this.destroy$), + shareReplay({ refCount: false, bufferSize: 1 }), + ), + ), ); const firstSetup$ = combineLatest([organization$, this.route.queryParams]).pipe( diff --git a/apps/web/src/app/vault/services/web-view-password-history.service.ts b/apps/web/src/app/vault/services/web-view-password-history.service.ts index 756c2140ab5..b1451b268de 100644 --- a/apps/web/src/app/vault/services/web-view-password-history.service.ts +++ b/apps/web/src/app/vault/services/web-view-password-history.service.ts @@ -1,9 +1,9 @@ import { Injectable } from "@angular/core"; +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; -import { ViewPasswordHistoryService } from "../../../../../../libs/common/src/vault/abstractions/view-password-history.service"; import { openPasswordHistoryDialog } from "../individual-vault/password-history.component"; /** diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 4e1d2322af3..a6f4e282b74 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Welke tipe item is dit?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Gevaarsone" }, - "dangerZoneDesc": { - "message": "Versigtig, hierdie aksies is onomkeerbaar!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Gebruik u domein se opgestelde allesomvattende inmandjie." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Lukraak", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "maand per lid" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index f8db7b496cc..78536d62d2c 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "ما هو نوع العنصر؟" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "منطقة خطرة" }, - "dangerZoneDesc": { - "message": "احذر، لن تستطيع التراجع عن هذه الإجراءات!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 9cc946e7b1e..e53908cf452 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Cəmi tətbiq" }, + "unmarkAsCriticalApp": { + "message": "Kritik tətbiq kimi işarəni götür" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Kritik tətbiq işarəsi uğurla götürüldü" + }, "whatTypeOfItem": { "message": "Bu elementin növü nədir?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Kimliyinizi doğrulayın" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanımırıq. Kimliyinizi doğrulamaq üçün e-poçtunuza göndərilən kodu daxil edin." + }, + "continueLoggingIn": { + "message": "Giriş etməyə davam" + }, "whatIsADevice": { "message": "Cihaz nədir?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Təhlükəli zona" }, - "dangerZoneDesc": { - "message": "Diqqətli olun, bu əməliyyatları geri qaytara bilməzsiniz!" - }, - "dangerZoneDescSingular": { - "message": "Diqqətli olun, bu əməliyyatı geri qaytara bilməzsiniz!" - }, "deauthorizeSessions": { "message": "Seansların səlahiyyətlərini götür" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Davam etsəniz, hazırkı seansınız bitəcək, təkrar giriş etməyiniz tələb olunacaq. Fəallaşdırılıbsa, iki addımlı giriş üçün yenidən soruşulacaq. Digər cihazlardakı aktiv seanslar, bir saata qədər aktiv qalmağa davam edə bilər." }, + "newDeviceLoginProtection": { + "message": "Yeni cihaz girişi" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Yeni cihaz girişi qorumasını söndür" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Yeni cihaz girişi qorumasını işə sal" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Yeni bir cihazdan giriş etdiyiniz zaman Bitwarden göndərən doğrulama e-poçtlarını dayandırmaq üçün aşağıdakı addımları izləyin." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Yeni bir cihazdan giriş etdiyiniz zaman Bitwarden-in sizə doğrulama e-poçtlarını göndərməsi üçün aşağıdakı addımları izləyin." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Yeni cihaz girişi qorumasını söndürsəniz, ana parolunuzu bilən hər kəs, istənilən cihazdan hesabınıza müraciət edə bilər. Hesabınızı doğrulama e-poçtları olmadan qorumaq üçün iki addımlı girişi qurun." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Yeni cihaz girişi qoruması dəyişiklikləri saxlanıldı" + }, "sessionsDeauthorized": { "message": "Bütün seansların səlahiyyəti götürüldü" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Domeninizin konfiqurasiya edilmiş hamısını yaxalama gələn qutusunu istifadə edin." }, + "useThisEmail": { + "message": "Bu e-poçtu istifadə et" + }, "random": { "message": "Təsadüfi", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Kolleksiya silinməsini sahibləri və adminləri ilə məhdudlaşdır" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Sahiblər və adminlər bütün kolleksiyaları və elementləri idarə edə bilər" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "üzv başına ay" }, + "monthPerMemberBilledAnnually": { + "message": "üzv üzrə aylıq illik hesablama" + }, "seats": { "message": "Yer" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Açar alqoritmi" }, + "sshPrivateKey": { + "message": "Private açar" + }, + "sshPublicKey": { + "message": "Public açar" + }, + "sshFingerprint": { + "message": "Barmaq izi" + }, "sshKeyFingerprint": { "message": "Barmaq izi" }, diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 02032485ce4..6d275bac7b2 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Які гэта элемент запісу?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Небяспечная зона" }, - "dangerZoneDesc": { - "message": "Асцярожна, гэтыя дзеянні з'яўляюцца незваротнымі!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Скасаваць аўтарызацыю сеанса" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Працягваючы, вы таксама выйдзіце з бягучага сеанса і вам неабходна будзе ўвайсці паўторна. Вы таксама атрымаеце паўторны запыт двухэтапнага ўваходу, калі гэта функцыя ў вас уключана. Сеансы на іншых прыладах могуць заставацца актыўнымі на працягу адной гадзіны." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Аўтарызацыя ўсіх сеансаў скасавана" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Выкарыстоўвайце сваю сканфігураваную скрыню для ўсёй пошты дамена." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Выпадкова", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index e5f5a0ff266..972b34bcf15 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Общо приложения" }, + "unmarkAsCriticalApp": { + "message": "Премахване на приложението от важните" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Приложението е премахнато от важните" + }, "whatTypeOfItem": { "message": "Вид на елемента" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Потвърдете самоличността си" }, + "weDontRecognizeThisDevice": { + "message": "Това устройство е непознато. Въведете кода изпратен на е-пощата Ви, за да потвърдите самоличността си." + }, + "continueLoggingIn": { + "message": "Продължаване с вписването" + }, "whatIsADevice": { "message": "Какво представлява едно устройство?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "ОПАСНО" }, - "dangerZoneDesc": { - "message": "Внимание, тези действия са необратими!" - }, - "dangerZoneDescSingular": { - "message": "Внимание, това действие е необратимо!" - }, "deauthorizeSessions": { "message": "Прекратяване на сесии" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Действието ще прекрати и текущата ви сесия, след което ще се наложи отново да се впишете. Ако сте включили двустепенна идентификация, ще се наложи да повторите и нея. Активните сесии на другите устройства може да останат такива до един час." }, + "newDeviceLoginProtection": { + "message": "Вписване от ново устройство" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Изключване на защитата за вписване от ново устройство" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Включване на защитата за вписване от ново устройство" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Продължете по-долу, за да изключите е-писмата за потвърждение, които Битуорден изпраща, когато се вписвате от ново устройство." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Продължете по-долу, за да посочите, че искате Биуорден да изпраща е-писма за потвърждение, когато се вписвате от ново устройство." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Ако защитата за вписване от ново устройство е изключена, всеки, който знае главната Ви парола, ще може да получи достъп до акаунта Ви от всяко устройство. Ако искате да защитите акаунта си без потвърждение чрез е-поща, настройте двустепенното удостоверяване." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Промените на защитата за вписване от ново устройство са запазени" + }, "sessionsDeauthorized": { "message": "Всички сесии са прекратени" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Използване на тази е-поща" + }, "random": { "message": "Произволно", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Ограничаване на изтриването на колекции, така че да може да се извършва само от собствениците и администраторите" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Собствениците и администраторите могат да управляват всички колекции и елементи" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "на месец за член" }, + "monthPerMemberBilledAnnually": { + "message": "месец, за всеки потребител, таксувано на годишна база" + }, "seats": { "message": "Места" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Алгоритъм на ключа" }, + "sshPrivateKey": { + "message": "Частен ключ" + }, + "sshPublicKey": { + "message": "Публичен ключ" + }, + "sshFingerprint": { + "message": "Отпечатък" + }, "sshKeyFingerprint": { "message": "Отпечатък" }, diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 0b9fbc99609..e4b8884de3d 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "এটি কোন ধরণের বস্তু?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 2e9d60fc4b2..cd3df3e0a1a 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Koja je ovo vrsta stavke?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 6693703f63b..09e533b2060 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -1,24 +1,24 @@ { "allApplications": { - "message": "Totes les notificacions" + "message": "Totes les aplicacions" }, "criticalApplications": { - "message": "Critical applications" + "message": "Aplicacions crítiques" }, "accessIntelligence": { - "message": "Access Intelligence" + "message": "Intel·ligència d'accés" }, "riskInsights": { - "message": "Risk Insights" + "message": "Coneixements de risc" }, "passwordRisk": { - "message": "Password Risk" + "message": "Risc de contrasenya" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Reviseu les contrasenyes de risc (febles, exposades o reutilitzades) a totes les aplicacions. Seleccioneu les aplicacions més crítiques per prioritzar les accions de seguretat perquè els usuaris aborden les contrasenyes de risc." }, "dataLastUpdated": { - "message": "Data last updated: $DATE$", + "message": "Última actualització de les dades: $DATE$", "placeholders": { "date": { "content": "$1", @@ -27,19 +27,19 @@ } }, "notifiedMembers": { - "message": "Notified members" + "message": "Membres notificats" }, "revokeMembers": { - "message": "Revoke members" + "message": "Revoca membres" }, "restoreMembers": { - "message": "Restore members" + "message": "Restaura membres" }, "cannotRestoreAccessError": { - "message": "Cannot restore organization access" + "message": "No es pot restaurar l'accés a l'organització" }, "allApplicationsWithCount": { - "message": "All applications ($COUNT$)", + "message": "Totes les aplicacions ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -48,10 +48,10 @@ } }, "createNewLoginItem": { - "message": "Create new login item" + "message": "Crea un nou element d'inici de sessió" }, "criticalApplicationsWithCount": { - "message": "Critical applications ($COUNT$)", + "message": "Aplicacions crítiques ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -60,7 +60,7 @@ } }, "notifiedMembersWithCount": { - "message": "Notified members ($COUNT$)", + "message": "Membres notificats ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -105,10 +105,10 @@ "message": "Request password change" }, "totalPasswords": { - "message": "Total passwords" + "message": "Contrasenyes totals" }, "searchApps": { - "message": "Search applications" + "message": "Cerca d'aplicacions" }, "atRiskMembers": { "message": "At-risk members" @@ -147,7 +147,7 @@ } }, "totalMembers": { - "message": "Total members" + "message": "Nombre total de membres" }, "atRiskApplications": { "message": "At-risk applications" @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Quin tipus d'element és aquest?" }, @@ -226,7 +232,7 @@ } }, "itemHistory": { - "message": "Item history" + "message": "Historial d'elements" }, "authenticatorKey": { "message": "Clau autenticadora" @@ -426,17 +432,17 @@ "message": "Booleà" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Casella de selecció" }, "cfTypeLinked": { "message": "Enllaçat", "description": "This describes a field that is 'linked' (related) to another field." }, "fieldType": { - "message": "Field type" + "message": "Tipus de camp" }, "fieldLabel": { - "message": "Field label" + "message": "Etiqueta del camp" }, "remove": { "message": "Suprimeix" @@ -502,7 +508,7 @@ "message": "Genera contrasenya" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Genera frase de pas" }, "checkPassword": { "message": "Comprova si la contrasenya ha estat exposada." @@ -605,7 +611,7 @@ "message": "Nota segura" }, "typeSshKey": { - "message": "SSH key" + "message": "Clau SSH" }, "typeLoginPlural": { "message": "Inicis de sessió" @@ -638,7 +644,7 @@ "message": "Nom complet" }, "address": { - "message": "Address" + "message": "Adreça" }, "address1": { "message": "Adreça 1" @@ -683,7 +689,7 @@ "message": "Visualitza l'element" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Nou $TYPE$", "placeholders": { "type": { "content": "$1", @@ -692,7 +698,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Edita $TYPE$", "placeholders": { "type": { "content": "$1", @@ -701,7 +707,7 @@ } }, "viewItemType": { - "message": "View $ITEMTYPE$", + "message": "Mostra $ITEMTYPE$", "placeholders": { "itemtype": { "content": "$1", @@ -755,7 +761,7 @@ } }, "copySuccessful": { - "message": "Copy Successful" + "message": "Còpia correcta" }, "copyValue": { "message": "Copia el valor", @@ -766,11 +772,11 @@ "description": "Copy password to clipboard" }, "copyPassphrase": { - "message": "Copy passphrase", + "message": "Copia frase de pas", "description": "Copy passphrase to clipboard" }, "passwordCopied": { - "message": "Password copied" + "message": "S'ha copiat la contrasenya" }, "copyUsername": { "message": "Copia el nom d'usuari", @@ -789,7 +795,7 @@ "description": "Copy URI to clipboard" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Copia $FIELD$", "placeholders": { "field": { "content": "$1", @@ -798,31 +804,31 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copia el lloc web" }, "copyNotes": { - "message": "Copy notes" + "message": "Copia notes" }, "copyAddress": { - "message": "Copy address" + "message": "Copia l'adreça" }, "copyPhone": { - "message": "Copy phone" + "message": "Copia telèfon" }, "copyEmail": { - "message": "Copy email" + "message": "Copia el correu electrònic" }, "copyCompany": { - "message": "Copy company" + "message": "Copia empresa" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Copia número de la Seguretat Social" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Copia el número de passaport" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Copia el número de llicència" }, "copyName": { "message": "Copia el nom" @@ -916,7 +922,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "S'han desplaçat elements a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -925,7 +931,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "S'ha desplaçat un element a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -979,25 +985,25 @@ "message": "Nivell d'accés" }, "accessing": { - "message": "Accessing" + "message": "Accedint a" }, "loggedOut": { "message": "Sessió tancada" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Heu tancat la sessió del compte." }, "loginExpired": { "message": "La vostra sessió ha caducat." }, "restartRegistration": { - "message": "Restart registration" + "message": "Reinicia el registre" }, "expiredLink": { "message": "Enllaç caducat" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "Reinicieu el registre o proveu d'iniciar sessió." }, "youMayAlreadyHaveAnAccount": { "message": "És possible que ja tingueu un compte" @@ -1027,7 +1033,7 @@ "message": "L'inici de sessió amb el dispositiu ha d'estar activat a la configuració de l'aplicació Bitwarden. Necessiteu una altra opció?" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Necessiteu una altra opció?" }, "loginWithMasterPassword": { "message": "Inici de sessió amb contrasenya mestra" @@ -1042,13 +1048,13 @@ "message": "Utilitzeu un mètode d'inici de sessió diferent" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Inicieu sessió amb la clau de pas" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Usa inici de sessió únic" }, "welcomeBack": { - "message": "Welcome back" + "message": "Benvingut/da de nou" }, "invalidPasskeyPleaseTryAgain": { "message": "Clau d'accés no vàlida. Torneu-ho a provar." @@ -1132,7 +1138,7 @@ "message": "Crea un compte" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nou a Bitwarden?" }, "setAStrongPassword": { "message": "Estableix una contrasenya segura" @@ -1150,22 +1156,28 @@ "message": "Inicia sessió" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Inicia sessió a Bitwarden" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Temps d'espera d'autenticació" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "La sessió d'autenticació s'ha esgotat. Reinicieu el procés d'inici de sessió." }, "verifyIdentity": { "message": "Verificació de la vostra identitat" }, + "weDontRecognizeThisDevice": { + "message": "No reconeixem aquest dispositiu. Introduïu el codi que us hem enviat al correu electrònic per verificar la identitat." + }, + "continueLoggingIn": { + "message": "Continua l'inici de sessió" + }, "whatIsADevice": { - "message": "What is a device?" + "message": "Què és un dispositiu?" }, "aDeviceIs": { - "message": "A device is a unique installation of the Bitwarden app where you have logged in. Reinstalling, clearing app data, or clearing your cookies could result in a device appearing multiple times." + "message": "Un dispositiu és una instal·lació única de l'aplicació Bitwarden on heu iniciat la sessió. Si torneu a instal·lar, suprimir les dades de l'aplicació o suprimir les galetes, pot ser que un dispositiu aparega diverses vegades." }, "logInInitiated": { "message": "S'ha iniciat la sessió" @@ -1201,7 +1213,7 @@ "message": "Pista de la contrasenya mestra (opcional)" }, "newMasterPassHint": { - "message": "New master password hint (optional)" + "message": "Pista de la contrasenya mestra (opcional)" }, "masterPassHintLabel": { "message": "Pista de la contrasenya mestra" @@ -1223,16 +1235,16 @@ "message": "Configuració" }, "accountEmail": { - "message": "Account email" + "message": "Correu electrònic del compte" }, "requestHint": { - "message": "Request hint" + "message": "Sol·licita pista" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Sol·licita pista de la contrasenya" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Introduïu l'adreça de correu electrònic del compte i se us enviarà la pista de contrasenya" }, "passwordHint": { "message": "Pista de la contrasenya" @@ -1272,10 +1284,10 @@ "message": "El vostre compte s'ha creat correctament. Ara ja podeu entrar." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "S'ha creat el vostre compte nou!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Heu iniciat sessió!" }, "trialAccountCreated": { "message": "Compte creat amb èxit." @@ -1296,7 +1308,7 @@ "message": "La caixa forta està bloquejada" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "El compte està bloquejat" }, "uuid": { "message": "UUID" @@ -1439,7 +1451,7 @@ "message": "Clau de seguretat OTP de Yubico" }, "yubiKeyDesc": { - "message": "Utilitzeu una YubiKey per accedir al vostre compte. Funciona amb els dispositius YubiKey 4, 4 Nano, 4C i NEO." + "message": "Utilitzeu un dispositiu YubiKey 4, 5 o NEO." }, "duoDescV2": { "message": "Enter a code generated by Duo Security.", @@ -1456,10 +1468,10 @@ "message": "Clau de seguretat FIDO U2F" }, "webAuthnTitle": { - "message": "FIDO2 WebAuthn" + "message": "Clau d'accés" }, "webAuthnDesc": { - "message": "Utilitzeu qualsevol clau de seguretat habilitada per WebAuthn per accedir al vostre compte." + "message": "Utilitzeu la biometria del vostre dispositiu o una clau de seguretat compatible amb FIDO2." }, "webAuthnMigrated": { "message": "(Migrat de FIDO)" @@ -1510,7 +1522,7 @@ "message": "Esteu segur que voleu continuar?" }, "moveSelectedItemsDesc": { - "message": "Trieu una carpeta a la que vulgueu afegir els $COUNT$ elements seleccionats.", + "message": "Trieu una carpeta a la qual vulgueu afegir els $COUNT$ elements seleccionats.", "placeholders": { "count": { "content": "$1", @@ -1697,22 +1709,22 @@ "message": "Historial de les contrasenyes" }, "generatorHistory": { - "message": "Generator history" + "message": "Historial del generador" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Neteja l'historial del generador" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Si continueu, totes les entrades se suprimiran permanentment de l'historial del generador. Esteu segur que voleu continuar?" }, "noPasswordsInList": { "message": "No hi ha cap contrasenya a llistar." }, "clearHistory": { - "message": "Clear history" + "message": "Neteja l'historial" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Res a mostrar" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Zona perillosa" }, - "dangerZoneDesc": { - "message": "Aneu amb compte, aquestes accions no són reversibles!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Desautoritza sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "El procediment també tancarà la sessió actual, i l'heu de tornar a iniciar. També demanarà iniciar la sessió en dues passes, si està habilitada. Les sessions actives d'altres dispositius poden mantenir-se actives fins a una hora." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Totes les sessions estan desautoritzades" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Utilitzeu la safata d'entrada global configurada del vostre domini." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Aleatori", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Els propietaris i els administradors poden gestionar totes les col·leccions i articles" }, @@ -8880,7 +8913,7 @@ "message": "Els ajustos dels seients es reflectiran en el pròxim cicle de facturació." }, "unassignedSeatsDescription": { - "message": "Seients de subscripció no assignats" + "message": "Places de subscripció no assignades" }, "purchaseSeatDescription": { "message": "Seients addicionals adquirits" @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "mes per membre" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seients" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 30a7c523d2a..d8aae6a2f0d 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Celkem aplikací" }, + "unmarkAsCriticalApp": { + "message": "Zrušit označení jako kritické aplikace" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Kritická aplikace úspěšně odoznačena" + }, "whatTypeOfItem": { "message": "O jaký typ položky se jedná?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Ověřte svou totožnost" }, + "weDontRecognizeThisDevice": { + "message": "Toto zařízení nepoznáváme. Zadejte kód zaslaný na Váš e-mail pro ověření Vaší totožnosti." + }, + "continueLoggingIn": { + "message": "Pokračovat v přihlášení" + }, "whatIsADevice": { "message": "Co je to zařízení?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Nebezpečná zóna" }, - "dangerZoneDesc": { - "message": "Opatrně. Tyto akce se nedají vrátit!" - }, - "dangerZoneDescSingular": { - "message": "Opatrně, tato akce je nevratná!" - }, "deauthorizeSessions": { "message": "Zrušit autorizaci relací" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Pokud chcete pokračovat, budete také odhlášeni z aktuální relace a bude nutné se znovu přihlásit. Pokud používáte dvoufázové přihlášení, bude také vyžadováno. Aktivní relace na jiných zařízeních mohou nadále zůstat aktivní po dobu až jedné hodiny." }, + "newDeviceLoginProtection": { + "message": "Nové přihlášení do zařízení" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Vypnout ochranu pro přihlášení z nového zařízení" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Zapnout ochranu pro přihlášení z nového zařízení" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Pokračujte níže a vypněte ověřovací e-maily od Bitwardenu, když se přihlásíte z nového zařízení." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Níže postupujte tak, aby Vám Bitwarden zasílal ověřovací e-maily při přihlášení z nového zařízení." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Pokud je ochrana přihlášení z nového zařízení vypnutá, může kdokoli s Vaším hlavním heslem přistupovat k Vašemu účtu z libovolného zařízení. Chcete-li svůj účet chránit bez ověřovacích e-mailů, nastavte dvoufázové přihlašování." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Změny ochrany přihlášení z nového zařízení byly uloženy" + }, "sessionsDeauthorized": { "message": "Všechny relace byly zrušeny" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Použijte nakonfigurovanou univerzální schránku své domény." }, + "useThisEmail": { + "message": "Použít tento e-mail" + }, "random": { "message": "Náhodně", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Omezí mazání kolekce na vlastníky a správce" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Vlastníci a správci mohou spravovat všechny kolekce a předměty" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "měsíčně za člena" }, + "monthPerMemberBilledAnnually": { + "message": "měsíčně za člena a účtováno ročně" + }, "seats": { "message": "Počet" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Algoritmus klíče" }, + "sshPrivateKey": { + "message": "Soukromý klíč" + }, + "sshPublicKey": { + "message": "Veřejný klíč" + }, + "sshFingerprint": { + "message": "Otisk prstu" + }, "sshKeyFingerprint": { "message": "Otisk prstu" }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index b0002e385a9..6d0c87ed1a0 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 403636653cf..a0d1674d181 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Applikationer i alt" }, + "unmarkAsCriticalApp": { + "message": "Afmarkér som kritisk app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Kritisk app afmarkeret" + }, "whatTypeOfItem": { "message": "Hvilken emnetype er denne?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Bekræft din identitet" }, + "weDontRecognizeThisDevice": { + "message": "Denne enhed er ikke genkendt. Angiv koden i den tilsendte e-mail for at bekræfte identiteten." + }, + "continueLoggingIn": { + "message": "Fortsæt med at logge ind" + }, "whatIsADevice": { "message": "Hvad er en enhed?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Farezone" }, - "dangerZoneDesc": { - "message": "Pas på, disse handlinger er irreversible!" - }, - "dangerZoneDescSingular": { - "message": "Forsigtig, denne handling er irreversibel!" - }, "deauthorizeSessions": { "message": "Fjern sessionsgodkendelser" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Fortsættes, logges man også ud af denne session og vil skulle logge ind igen. Der anmodes også om totrins-login igen, såfremt funktionen er opsat. Aktive sessioner på andre enheder kan forblive aktive i op til én time." }, + "newDeviceLoginProtection": { + "message": "Nyt enheds-login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Slå nyt enheds-login beskyttelse fra" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Slå nyt enheds-login beskyttelse til" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Fortsæt nedenfor for at deaktivere Bitwarden-bekræftelsesmails ved indlogning fra en ny enhed." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Fortsæt nedenfor for at få tilsendt Bitwarden-bekræftelsesmails ved indlogning fra en ny enhed." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Med den nye enheds-login beskyttelse slået fra kan alle med hovedadgangskoden tilgå brugerkontoen fra enhver enhed. For at beskytte brugerkontoen uden bekræftelsesmails, opsæt totrins-login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Ændringer i ny enheds-login beskyttelse er gemt" + }, "sessionsDeauthorized": { "message": "Godkendelser for alle sessioner fjernet" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Brug den for domænet opsatte Fang-alle indbakke." }, + "useThisEmail": { + "message": "Benyt denne e-mail" + }, "random": { "message": "Tilfældig", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Begræns samlingsslettelse til ejere og admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Ejere og admins kan håndtere alle samlinger og emner" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "måned pr. medlem" }, + "monthPerMemberBilledAnnually": { + "message": "måned pr. medlem faktureret årligt" + }, "seats": { "message": "Pladser" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Nøglealgoritme" }, + "sshPrivateKey": { + "message": "Privat nøgle" + }, + "sshPublicKey": { + "message": "Offentlig nøgle" + }, + "sshFingerprint": { + "message": "Fingeraftryk" + }, "sshKeyFingerprint": { "message": "Fingeraftryk" }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 89d49fe0c17..0ae02ed0977 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Anwendungen insgesamt" }, + "unmarkAsCriticalApp": { + "message": "Markierung als kritische Anwendung aufheben" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Markierung als kritische Anwendung erfolgreich aufgehoben" + }, "whatTypeOfItem": { "message": "Um welche Art von Eintrag handelt es sich hierbei?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verifiziere deine Identität" }, + "weDontRecognizeThisDevice": { + "message": "Wir erkennen dieses Gerät nicht. Gib den an deine E-Mail-Adresse gesendeten Code ein, um deine Identität zu verifizieren." + }, + "continueLoggingIn": { + "message": "Anmeldung fortsetzen" + }, "whatIsADevice": { "message": "Was ist ein Gerät?" }, @@ -1827,21 +1839,36 @@ "dangerZone": { "message": "Gefahrenzone" }, - "dangerZoneDesc": { - "message": "Vorsicht, diese Aktionen sind nicht umkehrbar!" - }, - "dangerZoneDescSingular": { - "message": "Vorsicht, diese Aktion ist nicht mehr rückgängig zu machen!" - }, "deauthorizeSessions": { "message": "Sitzungen abmelden" }, "deauthorizeSessionsDesc": { - "message": "Bist du besorgt, dass dein Konto auf einem anderen Gerät angemeldet ist? Fahre unten fort, um alle Computer oder Geräte, die du zuvor verwendet hast, zu deaktivieren. Dieser Sicherheitsschritt wird empfohlen, wenn du zuvor einen öffentlichen Computer verwendet oder dein Passwort versehentlich auf einem Gerät gespeichert hast, das dir nicht gehört. Dieser Schritt löscht auch alle zuvor gespeicherten zweistufigen Anmeldesitzungen." + "message": "Bist du besorgt, dass dein Konto auf einem anderen Gerät angemeldet ist? Fahre unten fort, um dich von allen Computern oder Geräten, die du zuvor verwendet hast, abzumelden. Dieser Sicherheitsschritt wird empfohlen, wenn du zuvor einen öffentlichen Computer verwendet oder dein Passwort versehentlich auf einem Gerät gespeichert hast, das dir nicht gehört. Dieser Schritt löscht auch alle zuvor gespeicherten zweistufigen Anmeldesitzungen." }, "deauthorizeSessionsWarning": { "message": "Wenn du fortfährst, wirst du auch von deiner aktuellen Sitzung abgemeldet, so dass du dich erneut anmelden musst. Du wirst auch aufgefordert, dich erneut in zwei Schritten anzumelden, falls dies eingerichtet ist. Aktive Sitzungen auf anderen Geräten können noch bis zu einer Stunde lang aktiv bleiben." }, + "newDeviceLoginProtection": { + "message": "Neue Geräteanmeldung" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Neuen Geräte-Anmeldeschutz deaktivieren" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Neuen Geräte-Anmeldeschutz aktivieren" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Fahre unten fort, um die Verifizierungs-E-Mails von Bitwarden zu deaktivieren, wenn du dich von einem neuen Gerät aus anmeldest." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Fahre unten fort, um Verifizierungs-E-Mails von Bitwarden zu erhalten, wenn du dich von einem neuen Gerät aus anmeldest." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Wenn der neue Geräte-Anmeldeschutz ausgeschaltet ist, kann jeder mit deinem Master-Passwort von jedem Gerät aus auf dein Konto zugreifen. Um dein Konto ohne Verifizierungs-E-Mails zu schützen, richte die Zwei-Faktor-Authentifizierung ein." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Änderungen am neuen Geräte-Anmeldeschutz gespeichert" + }, "sessionsDeauthorized": { "message": "Alle Sitzungen wurden abgemeldet" }, @@ -5786,7 +5813,7 @@ "message": "Bitwarden konnte folgende(n) Tresor-Eintrag/Einträge nicht entschlüsseln." }, "contactCSToAvoidDataLossPart1": { - "message": "Kontaktiere den Kundensupport", + "message": "Kontaktiere unser Customer Success Team", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Verwende den konfigurierten Catch-All-Posteingang deiner Domain." }, + "useThisEmail": { + "message": "Diese E-Mail-Adresse verwenden" + }, "random": { "message": "Zufällig", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Das Löschen von Sammlungen auf Eigentümer und Administratoren beschränken" }, + "limitItemDeletionDesc": { + "message": "Löschung von Einträgen auf Mitglieder mit der \"Darf verwalten\"-Berechtigung beschränken" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Besitzer und Administratoren können alle Sammlungen und Einträge verwalten" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "Monat pro Mitglied" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Benutzerplätze" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Schlüsselalgorithmus" }, + "sshPrivateKey": { + "message": "Privater Schlüssel" + }, + "sshPublicKey": { + "message": "Öffentlicher Schlüssel" + }, + "sshFingerprint": { + "message": "Fingerabdruck" + }, "sshKeyFingerprint": { "message": "Fingerabdruck" }, @@ -10092,7 +10137,7 @@ "message": "Dein Konto ist bei jedem der folgenden Geräte angemeldet. Wenn du ein Gerät nicht wiedererkennst, entferne es jetzt." }, "deviceListDescriptionTemp": { - "message": "Dein Konto wurde bei jedem der unter aufgeführten Geräte angemeldet." + "message": "Dein Konto wurde bei jedem der unten aufgeführten Geräte angemeldet." }, "claimedDomains": { "message": "Beanspruchte Domains" diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 4e0b435f4ea..cade0d1f120 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -3,22 +3,22 @@ "message": "Όλες οι εφαρμογές" }, "criticalApplications": { - "message": "Critical applications" + "message": "Κρίσιμες εφαρμογές" }, "accessIntelligence": { - "message": "Access Intelligence" + "message": "Πληροφορίες Πρόσβασης" }, "riskInsights": { - "message": "Risk Insights" + "message": "Insights Κινδύνου" }, "passwordRisk": { - "message": "Password Risk" + "message": "Ρίσκος Κωδικού Πρόσβασης" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Ελέξτε τους κωδικούς πρόσβασης (αδύναμους, εκτεθειμένους ή επαναχρησιμοποιούμενους) σε όλες τις εφαρμογές. Επιλέξτε τις πιο κρίσιμες εφαρμογές σας για να δώσετε προτεραιότητα στις ενέργειες ασφαλείας για τους χρήστες σας ώστε να αντιμετωπίσουν τους εκτεθειμένους κωδικούς πρόσβασης." }, "dataLastUpdated": { - "message": "Data last updated: $DATE$", + "message": "Τελευταία ενημέρωση δεδομένων: $DATE$", "placeholders": { "date": { "content": "$1", @@ -155,6 +155,12 @@ "totalApplications": { "message": "Σύνολο εφαρμογών" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Τι είδους στοιχείο είναι αυτό;" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Επαληθεύστε την ταυτότητά σας" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Επικίνδυνη Ζώνη" }, - "dangerZoneDesc": { - "message": "Προσοχή, αυτές οι ενέργειες είναι μη αναστρέψιμες!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Κατάργηση Εξουσιοδότησης Συνεδριών" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία και θα σας ζητήσει να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να παραμείνουν ενεργοποιημένες για έως και μία ώρα." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Η Ανακληθεί η Πρόσβαση από Όλες τις Συνεδρίες" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Χρησιμοποιήστε τα διαμορφωμένα εισερχόμενα catch-all του domain σας." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Τυχαίο", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Οι ιδιοκτήτες και οι διαχειριστές μπορούν να διαχειριστούν όλες τις συλλογές και τα στοιχεία" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "μήνας ανά μέλος" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Θέσεις" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0a4c91b6f10..f663a4c6397 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -176,6 +176,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -479,6 +485,18 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "baseDomain": { "message": "Base domain", "description": "Domain name. Example: website.com" @@ -743,15 +761,6 @@ "itemName": { "message": "Item name" }, - "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", - "placeholders": { - "collections": { - "content": "$1", - "example": "Work, Personal" - } - } - }, "ex": { "message": "ex.", "description": "Short abbreviation for 'example'." @@ -1182,6 +1191,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1191,6 +1206,9 @@ "logInInitiated": { "message": "Log in initiated" }, + "logInRequestSent": { + "message": "Request sent" + }, "submit": { "message": "Submit" }, @@ -1380,12 +1398,39 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "notificationSentDevicePart1": { + "message": "Unlock Bitwarden on your device or on the " + }, + "areYouTryingToAccessYourAccount": { + "message": "Are you trying to access your account?" + }, + "accessAttemptBy": { + "message": "Access attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "confirmAccess": { + "message": "Confirm access" + }, + "denyAccess": { + "message": "Deny access" + }, + "notificationSentDeviceAnchor": { + "message": "web app" + }, + "notificationSentDevicePart2": { + "message": "Make sure the Fingerprint phrase matches the one below before approving." + }, + "notificationSentDeviceComplete": { + "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + }, "aNotificationWasSentToYourDevice": { "message": "A notification was sent to your device" }, - "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" - }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -1780,7 +1825,7 @@ }, "requestPending": { "message": "Request pending" - }, + }, "logBackInOthersToo": { "message": "Please log back in. If you are using other Bitwarden applications log out and back in to those as well." }, @@ -1848,12 +1893,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1863,6 +1902,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message":"New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message":"Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message":"Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message":"Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message":"Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message":"With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -2164,8 +2224,20 @@ "manage": { "message": "Manage" }, - "canManage": { - "message": "Can manage" + "manageCollection": { + "message": "Manage collection" + }, + "viewItems": { + "message": "View items" + }, + "viewItemsHidePass": { + "message": "View items, hidden passwords" + }, + "editItems": { + "message": "Edit items" + }, + "editItemsHidePass": { + "message": "Edit items, hidden passwords" }, "disable": { "message": "Turn off" @@ -2362,11 +2434,8 @@ "twoFactorU2fProblemReadingTryAgain": { "message": "There was a problem reading the security key. Try again." }, - "twoFactorWebAuthnWarning": { - "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:" - }, - "twoFactorWebAuthnSupportWeb": { - "message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." + "twoFactorWebAuthnWarning1": { + "message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used." }, "twoFactorRecoveryYourCode": { "message": "Your Bitwarden two-step login recovery code" @@ -6740,6 +6809,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -7657,18 +7729,6 @@ "noCollection": { "message": "No collection" }, - "canView": { - "message": "Can view" - }, - "canViewExceptPass": { - "message": "Can view, except passwords" - }, - "canEdit": { - "message": "Can edit" - }, - "canEditExceptPass": { - "message": "Can edit, except passwords" - }, "noCollectionsAdded": { "message": "No collections added" }, @@ -8616,6 +8676,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -8729,11 +8792,11 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, - "grantAddAccessCollectionWarningTitle": { - "message": "Missing Can Manage Permissions" + "grantManageCollectionWarningTitle": { + "message": "Missing Manage Collection Permissions" }, - "grantAddAccessCollectionWarning": { - "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + "grantManageCollectionWarning": { + "message": "Grant Manage collection permissions to allow full collection management including deletion of collection." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." @@ -9821,6 +9884,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, @@ -10074,6 +10146,15 @@ "descriptorCode": { "message": "Descriptor code" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "importantNotice": { "message": "Important notice" }, @@ -10264,5 +10345,54 @@ "example": "Acme c" } } + }, + "accountDeprovisioningNotification" : { + "message": "Administrators now have the ability to delete member accounts that belong to a claimed domain." + }, + "deleteManagedUserWarningDesc": { + "message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action." + }, + "deleteManagedUserWarning": { + "message": "Delete is a new action!" + }, + "seatsRemaining": { + "message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.", + "placeholders": { + "remaining": { + "content": "$1", + "example": "5" + }, + "total": { + "content": "$2", + "example": "10" + } + } + }, + "existingOrganization": { + "message": "Existing organization" + }, + "selectOrganizationProviderPortal": { + "message": "Select an organization to add to your Provider Portal." + }, + "noOrganizations": { + "message": "There are no organizations to list" + }, + "yourProviderSubscriptionCredit": { + "message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription." + }, + "doYouWantToAddThisOrg": { + "message": "Do you want to add this organization to $PROVIDER$?", + "placeholders": { + "provider": { + "content": "$1", + "example": "Cool MSP" + } + } + }, + "addedExistingOrganization": { + "message": "Added existing organization" + }, + "assignedExceedsAvailable": { + "message": "Assigned seats exceed available seats." } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 8b765b7a697..bebc54d2a8a 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorise sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails Bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have Bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorised" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 8dd3e4d599e..14e17c15382 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognise this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorise sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if enabled. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails Bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have Bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorised" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 7651514110b..e2721fe3f3d 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Kiuspeca estas ĉi tiu ero?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danĝera Zono" }, - "dangerZoneDesc": { - "message": "Atentu, ĉi tiuj agoj ne estas reigeblaj!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Senrajtigi Sesiojn" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Se vi daŭrigos vian adiaŭadon de la nuna seanco, necesos vin saluti denove. Oni ankaŭ demandos de vi du-faktoran aŭtentigon, se tiu elekteblo estas ebligita. La seancoj aktivaj sur aliaj aparatoj povas resti daŭre aktivaj ankoraŭ unu horon." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Ĉiuj Sesioj Neaŭtorizitaj" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Hazarda", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 863fba66ec6..c93410b4e49 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "¿Qué tipo de elemento es este?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verifica tu identidad" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Zona peligrosa" }, - "dangerZoneDesc": { - "message": "¡Cuidado, estas acciones no son reversibles!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Desautorizar sesiones" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceder también cerrará tu sesión actual, requiriendo que vuelvas a identificarte. También se te pedirá nuevamente tu autenticación en dos pasos en caso de que la tengas habilitada. Las sesiones activas en otros dispositivos pueden mantenerse activas hasta una hora más." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Desautorizadas todas las sesiones" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Utiliza la bandeja de entrada global configurada de tu dominio." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Aleatorio", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Propietarios y administradores pueden gestionar todas las colecciones y elementos" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 84f6812d8dc..52c61c301b2 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Mis tüüpi kirje see on?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Kinnitage oma Identiteet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Ohtlik tsoon" }, - "dangerZoneDesc": { - "message": "Ettevaatust, neid toiminguid ei saa tagasi võtta!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Sessioonide tühistamine" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Jätkatest logitakse sind ka käimasolevast sessioonist välja, mistõttu pead kontosse uuesti sisse logima. Lisaks võidakse küsida kaheastmelist kinnitust, kui see on sisse lülitatud. Teised kontoga ühendatud seadmed võivad jääda sisselogituks kuni üheks tunniks." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Kõikidest seadmetest on välja logitud" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Kasuta domeenipõhist kogumisaadressi." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Juhuslik", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index d7b5f91bc63..8e4d12fb1f4 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Zein elementu mota da hau?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Eremu arriskutsua" }, - "dangerZoneDesc": { - "message": "Kontuz, ekintza hauek ez dira itzulgarriak!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Baimena kendu saio hasierei" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Jarraitzeak uneko saioa itxiko du eta berriro saioa hasteko eskatuko zaizu. Gaituta badago, berriz ere bi urratseko saioa hasiera eskatuko zaizu. Beste gailu batzuetako saio aktiboek ordubete iraun dezakete aktibo." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Saio guztiei baimena kendua" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Erabili zure domeinuan konfiguratutako sarrerako ontzia." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Ausazkoa", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 7590cc7b545..51d862e5314 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "این چه نوع موردی است؟" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "منطقه‌ی خطر" }, - "dangerZoneDesc": { - "message": "مراقب باشید، این اقدامات قابل برگشت نیستند!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "لغو مجوز نشست‌ها" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "ادامه دادن همچنین شما را از نشست فعلی خود خارج می‌کند و باید دوباره وارد سیستم شوید. در صورت راه اندازی مجدداً از شما خواسته می‌شود تا دوباره به سیستم دو مرحله ای بپردازید. جلسات فعال در دستگاه‌های دیگر ممکن است تا یک ساعت فعال بمانند." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "همه نشست‌ها غیرمجاز است" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "از صندوق ورودی پیکربندی شده دامنه خود استفاده کنید." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "تصادفی", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 604e9ecadce..a1d909455c9 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Sovelluksia yhteensä" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Minkä tyyppinen kohde tämä on?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Vahvista henkilöllisyytesi" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Vaaravyöhyke" }, - "dangerZoneDesc": { - "message": "Ole varovainen! Näitä toimintoja ei ole mahdollista kumota!" - }, - "dangerZoneDescSingular": { - "message": "Ole varoivainen. Tätä ei ole mahdollista perua!" - }, "deauthorizeSessions": { "message": "Mitätöi kaikki istunnot" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Jatkaminen uloskirjaa myös nykyisen istunnon pakottaen uudelleenkirjautumisen sekä kaksivaiheinen kirjautumisen, jos se on määritetty. Muiden laitteiden aktiiviset istunnot saattavat toimia vielä tunnin ajan." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Kaikki istunnot mitätöitiin" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Käytä verkkotunnuksesi catch-all-postilaatikkoa." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Satunnainen", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Rajoita kokoelmien poisto omistajille ja ylläpitäjille" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Omistajat ja ylläpitäjät voivat hallita kaikkia kokoelmia ja kohteita" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "kuukaudessa/jäsen" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Käyttäjäpaikat" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Avainalgoritmi" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Sormenjälki" }, diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 095d5b05565..cebcaa22aef 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Ano'ng uri ng item na ito?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Mapanganib na lugar" }, - "dangerZoneDesc": { - "message": "Mag-ingat, wala nang bawian sa mga gagawin mo rito!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "I-deauthorize ang mga sesyon" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Mala-log out ka rin sa kasalukuyan mong sesyon kung tutuloy ka, at kakailanganin mong mag-log in ulit. Kakailanganin mo ring ulitin ang dalawang-hakbang na pag-log in kung naka-set up ito. Maaaring manatiling aktibo ang mga sesyon sa iba pang device nang hanggang isang oras." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Na-deauthorize lahat ng mga sesyon" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Gamitin ang naka configure na inbox ng catch all ng iyong domain." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index e7d2df4da50..c477a5d027d 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total des applications" }, + "unmarkAsCriticalApp": { + "message": "Ne plus marquer comme application critique" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Marquage d'application critique retiré avec succès" + }, "whatTypeOfItem": { "message": "Quel type d'élément est-ce ?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Vérifiez votre Identité" }, + "weDontRecognizeThisDevice": { + "message": "Nous ne reconnaissons pas cet appareil. Entrez le code envoyé à votre courriel pour vérifier votre identité." + }, + "continueLoggingIn": { + "message": "Continuer à vous connecter" + }, "whatIsADevice": { "message": "Qu'est-ce qu'un appareil ?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Zone de danger" }, - "dangerZoneDesc": { - "message": "Attention, ces actions sont irréversibles !" - }, - "dangerZoneDescSingular": { - "message": "Attention, cette action est irréversible!" - }, "deauthorizeSessions": { "message": "Révoquer les sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "En poursuivant, vous serez également déconnecté de votre session en cours, ce qui vous obligera à vous reconnecter. Vous serez également invité à vous reconnecter via l'authentification à deux facteurs, si elle est configurée. Les sessions actives sur d'autres appareils peuvent rester actives pendant encore une heure." }, + "newDeviceLoginProtection": { + "message": "Connexion à un nouvel appareil" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Désactivez la protection de connexion du nouvel appareil" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Activez la protection de connexion du nouvel appareil" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Procédez ci-dessous pour désactiver les courriels de vérification envoyés par Bitwarden lorsque vous vous connectez à partir d'un nouvel appareil." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Procédez ci-dessous pour que Bitwarden vous envoie des courriels de vérification lorsque vous vous connectez à partir d'un nouvel appareil." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Avec la protection de connexion d'un nouvel appareil désactivée, toute personne ayant votre mot de passe maître peut accéder à votre compte depuis n'importe quel appareil. Pour protéger votre compte sans courriel de vérification, configurez la connexion en deux étapes." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Modifications de la protection de connexion de l'appareil enregistrées" + }, "sessionsDeauthorized": { "message": "Toutes les sessions ont été révoquées" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Utilisez la boîte de réception du collecteur (catch-all) configurée de votre domaine." }, + "useThisEmail": { + "message": "Utiliser ce courriel" + }, "random": { "message": "Aléatoire", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limiter la suppression de collections aux propriétaires et administrateurs" }, + "limitItemDeletionDesc": { + "message": "Limite la suppression de l'élément aux membres avc l'autorisation Peut gérer" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Les propriétaires et les administrateurs peuvent gérer toutes les collections et tous les éléments" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "mois par membre" }, + "monthPerMemberBilledAnnually": { + "message": "mois par membre facturés annuellement" + }, "seats": { "message": "Licences" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Algorithme de clé" }, + "sshPrivateKey": { + "message": "Clé privée" + }, + "sshPublicKey": { + "message": "Clé publique" + }, + "sshFingerprint": { + "message": "Empreinte digitale" + }, "sshKeyFingerprint": { "message": "Empreinte digitale" }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 39cc3fd0841..b7baf7097ae 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Que tipo de elemento é este?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index bc831bf501c..fe6b6008f66 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "מאיזה סוג פריט זה?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "אזור מסוכן" }, - "dangerZoneDesc": { - "message": "זהירות, פעולות אלה לא ניתנות לביטול!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "בטל הרשאות סשנים" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "בכדי להמשיך הסשן הנוכחי ינותק, ותדרש להזין את פרטי הכניסה החדשים וגם את פרטי האימות הדו-שלבי, אם הוא מאופשר. כל הסשנים הפעילים במכשירים אחרים ישארו פעילים עד שעה ממועד הכניסה החדשה." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "הוסרה ההרשאה מכל הסשנים" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 1da5e61f6d1..f23ad758ea6 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "यह किस प्रकार का आइटम है?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 9f64a64fdc9..b157f158a53 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Ukupno aplikacija" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Koja je ovo vrsta stavke?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Potvrdi svoj identitet" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "OPASNA zona!" }, - "dangerZoneDesc": { - "message": "Pažljivo, ove akcije su konačne i ne mogu se poništiti!" - }, - "dangerZoneDescSingular": { - "message": "Pažljivo, ova radnja se ne može poništiti!" - }, "deauthorizeSessions": { "message": "Deautoriziraj sesije" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Ako nastaviš, trenutna sesija će biti zatvorena, što će zahtijevati ponovnu prijavu uklljučujući i prijavu dvostrukom autentifikacijom, ako je ona aktivna. Aktivne sesije na drugim uređajima mogu ostati aktivne još jedan sat." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Sve sesije deautorizirane" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Koristi konfigurirani catch-all sandučić svoje domene." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Nasumično", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Omogući brisanje zbirki samo vlasnicima i adminima" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Vlasnici i admini mogu upravljati svim zbirkama i stavkama" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "mjesečno po korisniku" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Mjesta" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Algoritam ključa" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Otisak prsta" }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index d8d24fc43b8..64f5c158a9e 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Összes alkalmazás" }, + "unmarkAsCriticalApp": { + "message": "Ktritkus alkalmazás jelölés eltávolítása" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "A ktritkus alkalmazás jelölés eltávolítása sikeres volt." + }, "whatTypeOfItem": { "message": "Milyen típusú elem ez?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Személyazonosság ellenőrzése" }, + "weDontRecognizeThisDevice": { + "message": "Nem ismerhető fel ez az eszköz. Írjuk be az email címünkre küldött kódot a személyazonosság igazolásához." + }, + "continueLoggingIn": { + "message": "A bejelentkezés folytatása" + }, "whatIsADevice": { "message": "Mi az eszköz?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Veszélyes terület" }, - "dangerZoneDesc": { - "message": "Óvatosan! Ezeket a műveleteket nem lehet visszaállítani." - }, - "dangerZoneDescSingular": { - "message": "Óvatosan! Ezeket a műveleteket nem lehet visszaállítani." - }, "deauthorizeSessions": { "message": "Munkamenetek hitelesítésének eldobása" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "A folytatásban s felhasználó kiléptetésre kerül az aktuális munkamenetből, szükséges az ismételt bejelentkezés. Ismételten megjelenik a kétlépcsős bejelentkezés, ha az engedélyezett. Más eszközök aktív munkamenetei akár egy óráig is aktívak maradhatnak." }, + "newDeviceLoginProtection": { + "message": "Új eszköz bejelentkezés" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Az új eszköz bejelentkezési védelmének kikapcsolása" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Az új eszköz bejelentkezési védelmének bekapcsolása" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Folytassuk az alábbiakkal a Bitwarden által küldendő ellenőrző emailek kikapcsolásához, amikor új eszközről jelentkezünk be." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Folytassuk az alábbiakkal a Bitwarden által küldendő ellenőrző emailek bekapcsolásához, amikor új eszközről jelentkezünk be." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Ha az új eszköz bejelentkezési védelme ki van kapcsolva, a mesterjelszó birtokában bárki hozzáférhet fiókhoz bármilyen eszközről. Ha meg szeretnénk védeni fiókot ellenőrző emailek nélkül, állítsunk be kétlépcsős bejelentkezést." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Az új eszköz bejelentkezés védelmi módosítások mentésre kerültek." + }, "sessionsDeauthorized": { "message": "Az összes munkamenet hitelesítése eldobásra került." }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Használjuk a tartomány konfigurált összes befogási bejövő postaládát." }, + "useThisEmail": { + "message": "Ezen email használata" + }, "random": { "message": "Véletlen", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "A gyűjtemény törlésének korlátozása tulajdonosokra és adminisztrátorokra" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "hónap tagonként éves számlázással" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Kulcs algoritmus" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Ujjlenyomat" }, diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 1e796e4d36d..1fd1632e306 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Jenis barang apa ini?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Zona Bahaya" }, - "dangerZoneDesc": { - "message": "Hati-hati, tindakan ini tidak bisa dibatalkan!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Batalkan Otorisasi Sesi" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Melanjutkan juga akan mengeluarkan Anda dari sesi saat ini, mengharuskan Anda untuk masuk kembali. Anda juga akan diminta untuk masuk dua langkah lagi, jika diaktifkan. Sesi aktif di perangkat lain dapat terus aktif hingga satu jam." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Semua Sesi Dicabut Izinnya" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Acak", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 62a4044039d..082b087120c 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Che tipo di elemento è questo?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verifica la tua identità" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "Cos'è un dispositivo?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Zona pericolosa" }, - "dangerZoneDesc": { - "message": "Attento, queste azioni non sono reversibili!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Annulla autorizzazione sessioni" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Inoltre, procedere ti farà uscire dalla sessione corrente, richiedendoti di accedere. Se attivata, dovrai completare la verifica in due passaggi di nuovo. Le sessioni attive su altri dispositivi potrebbero continuare a rimanere attive per un massimo di un'ora." }, + "newDeviceLoginProtection": { + "message": "Accesso nuovo dispositivo" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Disattiva la nuova protezione di accesso del dispositivo" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Attiva la nuova protezione di accesso del dispositivo" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Procedi qui sotto per disattivare le e-mail di verifica che Bitwarden invia quando accedi da un nuovo dispositivo." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Procedi qui sotto per far sì che Bitwarden invii e-mail di verifica quando accedi da un nuovo dispositivo." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Con la nuova protezione di accesso del dispositivo disattivata, chiunque con la tua parola d'accesso principale può accedere al tuo account da qualsiasi dispositivo. Per proteggere il tuo account senza e-mail di verifica, imposta l'accesso in due passaggi." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Modifiche alla protezione del nuovo dispositivo salvate" + }, "sessionsDeauthorized": { "message": "Tutte le sessioni revocate" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Usa la casella di posta catch-all di dominio." }, + "useThisEmail": { + "message": "Usa questa e-mail" + }, "random": { "message": "Casuale", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Proprietari e amministratori possono gestire tutte le raccolte e gli elementi" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "mese per membro" }, + "monthPerMemberBilledAnnually": { + "message": "mese per membro fatturato annualmente" + }, "seats": { "message": "Slot" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Chiave privata" + }, + "sshPublicKey": { + "message": "Chiave pubblica" + }, + "sshFingerprint": { + "message": "Impronta digitale" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index e4255c93644..b7ca1764125 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "合計アプリ数" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "このアイテムのタイプは何ですか?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "本人確認" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "危険な操作" }, - "dangerZoneDesc": { - "message": "これらの操作はやり直せないため注意してください!" - }, - "dangerZoneDescSingular": { - "message": "この操作は元に戻せないため注意してください!" - }, "deauthorizeSessions": { "message": "セッションの承認を取り消す" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "次に進むと現在のセッションからログアウトし二段階認証を含め再度ログインが必要になります。他のデバイスでのセッションは1時間程度維持されます。" }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "全てのセッションを無効化" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "ドメインに設定されたキャッチオール受信トレイを使用します。" }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "ランダム", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "コレクションの削除を所有者と管理者のみに制限" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "所有者と管理者はすべてのコレクションとアイテムを管理できます" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "月/メンバーあたり" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "シート" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 4c525c7f993..40b717edb69 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Რა სახის საგანია ეს?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index bb7d3b657b5..3b8d147b95f 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index de8bcbfd2c8..18c40d79744 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "ಇದು ಯಾವ ರೀತಿಯ ಐಟಂ?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "ಅಪಾಯ ವಲಯ" }, - "dangerZoneDesc": { - "message": "ಎಚ್ಚರಿಕೆಯಿಂದ, ಈ ಕ್ರಿಯೆಗಳು ಹಿಂತಿರುಗಿಸಲಾಗುವುದಿಲ್ಲ!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "ಸೆಷನ್‌ಗಳನ್ನು ಅನಧಿಕೃತಗೊಳಿಸಿ" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "ಮುಂದುವರಿಯುವುದರಿಂದ ನಿಮ್ಮ ಪ್ರಸ್ತುತ ಸೆಷನ್‌ನಿಂದ ನಿಮ್ಮನ್ನು ಲಾಗ್ಔಟ್ ಮಾಡುತ್ತದೆ, ನಿಮಗೆ ಮತ್ತೆ ಲಾಗ್ ಇನ್ ಆಗುವ ಅಗತ್ಯವಿರುತ್ತದೆ. ಸಕ್ರಿಯಗೊಳಿಸಿದ್ದರೆ ಮತ್ತೆ ಎರಡು-ಹಂತದ ಲಾಗಿನ್‌ಗೆ ನಿಮ್ಮನ್ನು ಕೇಳಲಾಗುತ್ತದೆ. ಇತರ ಸಾಧನಗಳಲ್ಲಿ ಸಕ್ರಿಯ ಸೆಷನ್‌ಗಳು ಒಂದು ಗಂಟೆಯವರೆಗೆ ಸಕ್ರಿಯವಾಗಿ ಮುಂದುವರಿಯಬಹುದು." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "ಎಲ್ಲಾ ಸೆಷನ್‌ಗಳು ಅನಧಿಕೃತವಾಗಿವೆ" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index c4122e12c28..c7f80ff1cd1 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "항목의 유형이 무엇입니까?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "위험 구역" }, - "dangerZoneDesc": { - "message": "주의, 이 행동들은 되돌릴 수 없음!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "세션 해제" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "계속 진행하면, 현재 세션 또한 로그아웃 되므로 다시 로그인하여야 합니다. 2단계 로그인이 활성화 된 경우 다시 요구하는 메세지가 표시됩니다. 다른 기기의 활성화 된 세션은 최대 1시간 동안 유지 될 수 있습니다." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "모든 세션 해제 됨" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 48531c50d07..c29227d3db1 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Kopējais lietotņu skaits" }, + "unmarkAsCriticalApp": { + "message": "Noņemt kritiskas lietontes atzīmi" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Kritiskas lietotnes atzīme sekmīgi noņemta" + }, "whatTypeOfItem": { "message": "Kāda veida vienums tas ir?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Jāapliecina sava identitāte" }, + "weDontRecognizeThisDevice": { + "message": "Mēs neatpazīstam šo ierīci. Jāievada kods, kas tika nosūtīts e-pastā, lai apliecinātu savu identitāti." + }, + "continueLoggingIn": { + "message": "Turpināt pieteikšanos" + }, "whatIsADevice": { "message": "Kas ir ierīce?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Bīstamā sadaļa" }, - "dangerZoneDesc": { - "message": "Piesardzību, šīs darbības nav atsaucamas!" - }, - "dangerZoneDescSingular": { - "message": "Uzmanīgi, šī darbība ir neatgriezeniska!" - }, "deauthorizeSessions": { "message": "Padarīt sesijas spēkā neesošas" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Tiks veikta atteikšanās no pašreizējās sesijas, un pēc tam būs nepieciešams atkārtoti pieteikties. Būs nepieciešama arī divpakāpju pieteikšanās, ja tā ir iespējota. Citās ierīcēs darbojošās sesijas var būt spēkā līdz vienai stundai." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Visu sesiju darbība ir atsaukta" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Izmantot uzstādīto domēna visu tverošo iesūtni." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Nejauši", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Ļaut krājumu izdzēšanu tikai īpašniekiem un pārvaldītājiem" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Īpašnieki un pārvaldnieki var pārvaldīt visus krājumus un vienumus" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "mēnesī par dalībnieku" }, + "monthPerMemberBilledAnnually": { + "message": "par dalībnieku mēnesī ar ikgadēju rēķinu" + }, "seats": { "message": "Vietas" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Atslēgas algoritms" }, + "sshPrivateKey": { + "message": "Privātā atslēga" + }, + "sshPublicKey": { + "message": "Publiskā atslēga" + }, + "sshFingerprint": { + "message": "Pirkstu nospiedums" + }, "sshKeyFingerprint": { "message": "Pirkstu nospiedums" }, diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 00a28c35e2e..34ef4b72023 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "ഇത് ഏതു തരം ഇനം ആണ്?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "അപകട മേഖല" }, - "dangerZoneDesc": { - "message": "ശ്രദ്ധിക്കുക, ഈ പ്രവർത്തനങ്ങൾ മാറ്റാനാവില്ല!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize Sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if enabled. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "എല്ലാ സെഷനും നിരസിച്ചു." }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index bb7d3b657b5..3b8d147b95f 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index bb7d3b657b5..3b8d147b95f 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 020c208fda8..621f5de51a9 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Hva slags element er dette?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Faresone" }, - "dangerZoneDesc": { - "message": "Vær forsiktig, disse handlingene kan ikke reverseres!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Fjern autorisering av økter" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Å fortsette vil også logge deg av din nåværende økt, og gjør at du vil måtte logge på igjen. Du vil også bli bedt om 2-trinnsinnlogging igjen, dersom det er aktivert. Aktive økter på andre enheter kan kanskje forbli aktive i opptil en time." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Fjernet autoriseringen fra alle økter" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Bruk domenets konfigurerte catch-all innboks." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Vilkårlig", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 54ca22dca15..60c19be21fc 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "यो कस्तो प्रकारको वस्तु हो?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index d1fb95104cb..fa40759c62a 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Totaal applicaties" }, + "unmarkAsCriticalApp": { + "message": "Markeren als belangrijke app ongedaan maken" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Markeren als belangrijke app ongedaan gemaakt" + }, "whatTypeOfItem": { "message": "Van welke categorie is dit item?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Controleer je identiteit" }, + "weDontRecognizeThisDevice": { + "message": "We herkennen dit apparaat niet. Voer de code in die naar je e-mail is verzonden om je identiteit te verifiëren." + }, + "continueLoggingIn": { + "message": "Doorgaan met inloggen" + }, "whatIsADevice": { "message": "Wat is een apparaat?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Gevarenzone" }, - "dangerZoneDesc": { - "message": "Waarschuwing - deze acties zijn niet terug te draaien!" - }, - "dangerZoneDescSingular": { - "message": "Waarschuwing - deze actie is niet terug te draaien!" - }, "deauthorizeSessions": { "message": "Sessie-autorisaties intrekken" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Doorgaan zal je huidige sessie uitloggen, waarna je opnieuw moet inloggen. Je moet ook je tweestapsaanmelding opnieuw doorlopen, als die is ingeschakeld. Actieve sessies op andere apparaten blijven mogelijk nog een uur actief." }, + "newDeviceLoginProtection": { + "message": "Nieuwe apparaat login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Inlogbescherming nieuwe apparaten uitschakelen" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Inlogbescherming nieuwe apparaten inschakelen" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Ga hieronder verder voor het uitschakelen van de e-mailverificatieberichten die Bitwarden stuurt wanneer je inlogt vanaf een nieuw apparaat." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Ga hieronder verder om Bitwarden verificatie e-mails te sturen wanneer je inlogt vanaf een nieuw apparaat.\n\nGa hieronder verder om Bitwarden e-mailverificatieberichten te laten sturen wanneer je inlogt vanaf een nieuw apparaat." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Als je inlogbescherming voor nieuwe apparaten uitschakelt, kan iedereen op ieder apparaat met je hoofdwachtwoord inloggen. Stel tweestapsaanmelding in om je account te beschermen zonder e-mailverificatieberichten." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Wijzigingen inlogbescherming nieuwe apparaten opgeslagen" + }, "sessionsDeauthorized": { "message": "Autorisatie van alle sessies ingetrokken" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Gebruik de catch-all inbox van je domein." }, + "useThisEmail": { + "message": "Dit e-mailadres gebruiken" + }, "random": { "message": "Willekeurig", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Verwijderen van collecties beperken tot eigenaren en managers" }, + "limitItemDeletionDesc": { + "message": "Beperk het verwijderen van items tot leden met het recht Kan beheren" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Eigenaren en beheerders kunnen alle collecties en items beheren" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "maand per lid" }, + "monthPerMemberBilledAnnually": { + "message": "maand per lid jaarlijks gefactureerd" + }, "seats": { "message": "Personen" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Sleutelalgoritme" }, + "sshPrivateKey": { + "message": "Privé sleutel" + }, + "sshPublicKey": { + "message": "Publieke sleutel" + }, + "sshFingerprint": { + "message": "Vingerafdruk" + }, "sshKeyFingerprint": { "message": "Vingerafdruk" }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 09c74757eec..10547f94847 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Kva type oppføring er dette?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index bb7d3b657b5..3b8d147b95f 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index c4877b6b0f8..a299723f594 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Jakiego rodzaju jest to element?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Zweryfikuj swoją tożsamość" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Niebezpieczna strefa" }, - "dangerZoneDesc": { - "message": "Uwaga - te operacje są nieodwracalne!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Zakończ sesje" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Ta czynność spowoduje wylogowanie z bieżącej sesji, przez co konieczne będzie ponowne zalogowanie się. Zostaniesz również poproszony o ponowne logowanie dwustopniowe, jeśli masz włączoną tę opcję. Aktywne sesje na innych urządzeniach mogą pozostać aktywne przez maksymalnie godzinę." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Wszystkie sesje zostały zakończone" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Użyj skonfigurowanej skrzynki catch-all w swojej domenie." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Losowa", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Właściciele i administratorzy mogą zarządzać wszystkimi zbiorami i elementami" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "miesięcznie za członka" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Miejsca" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 13f7203246b..4b9e65535ce 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Todos os aplicativos" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Que tipo de item é este?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verifique sua identidade" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Zona de perigo" }, - "dangerZoneDesc": { - "message": "Cuidado, essas ações não são reversíveis!" - }, - "dangerZoneDescSingular": { - "message": "Cuidado, esta ação não é reversível!" - }, "deauthorizeSessions": { "message": "Desautorizar sessões" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "O processo também desconectará você da sua sessão atual, exigindo que você inicie a sessão novamente. Você também será solicitado a efetuar login em duas etapas novamente, se estiver ativado. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Todas as Sessões Desautorizadas" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use o catch-all configurado no seu domínio." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Aleatório", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limitar exclusão de coleção a proprietários e administradores" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Proprietários e administradores podem gerenciar todas as coleções e itens" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "mês por membro" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Lugares" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Algoritmo da chave" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Impressão digital" }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 14a5de13e79..6d724302888 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Todas de aplicações" }, + "unmarkAsCriticalApp": { + "message": "Desmarcar como aplicação crítica" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Aplicação crítica desmarcada com sucesso" + }, "whatTypeOfItem": { "message": "Que tipo de item é este?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verifique a sua identidade" }, + "weDontRecognizeThisDevice": { + "message": "Não reconhecemos este dispositivo. Introduza o código enviado para o seu e-mail para verificar a sua identidade." + }, + "continueLoggingIn": { + "message": "Continuar a iniciar sessão" + }, "whatIsADevice": { "message": "O que é um dispositivo?" }, @@ -1207,7 +1219,7 @@ "message": "Dica da palavra-passe mestra" }, "masterPassHintText": { - "message": "Se se esquecer da sua palavra-passe, a dica da palavra-passe pode ser enviada para o seu e-mail. Máximo de $CURRENT$/$MAXIMUM$ caracteres.", + "message": "Se se esquecer da sua palavra-passe, a dica da palavra-passe pode ser enviada para o seu e-mail. Máximo de $CURRENT$/$MAXIMUM$ carateres.", "placeholders": { "current": { "content": "$1", @@ -1256,7 +1268,7 @@ "message": "É necessário reescrever a palavra-passe mestra." }, "masterPasswordMinlength": { - "message": "A palavra-passe mestra deve ter pelo menos $VALUE$ caracteres.", + "message": "A palavra-passe mestra deve ter pelo menos $VALUE$ carateres.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -1641,15 +1653,15 @@ "message": "Mínimo de números" }, "minSpecial": { - "message": "Mínimo de caracteres especiais", + "message": "Mínimo de carateres especiais", "description": "Minimum special characters" }, "ambiguous": { - "message": "Evitar caracteres ambíguos", + "message": "Evitar carateres ambíguos", "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Evitar caracteres ambíguos", + "message": "Evitar carateres ambíguos", "description": "Label for the avoid ambiguous characters checkbox." }, "regeneratePassword": { @@ -1674,7 +1686,7 @@ "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caracteres especiais (!@#$%^&*)" + "message": "Carateres especiais (!@#$%^&*)" }, "numWords": { "message": "Número de palavras" @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Zona de risco" }, - "dangerZoneDesc": { - "message": "Cuidado, estas ações são irreversíveis!" - }, - "dangerZoneDescSingular": { - "message": "Cuidado, esta ação não é reversível!" - }, "deauthorizeSessions": { "message": "Desautorizar sessões" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Ao prosseguir, também terminará a sua sessão atual e terá de iniciar sessão novamente. Ser-lhe-á também pedido que volte efetuar a verificação de dois passos, se estiver configurada. As sessões ativas noutros dispositivos podem continuar ativas até uma hora." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Todas as sessões desautorizadas" }, @@ -4708,16 +4735,16 @@ } }, "policyInEffectUppercase": { - "message": "Contém um ou mais caracteres em maiúsculas" + "message": "Contém um ou mais carateres em maiúsculas" }, "policyInEffectLowercase": { - "message": "Contém um ou mais caracteres em minúsculas" + "message": "Contém um ou mais carateres em minúsculas" }, "policyInEffectNumbers": { "message": "Contém um ou mais números" }, "policyInEffectSpecial": { - "message": "Contém um ou mais dos seguintes caracteres especiais $CHARS$", + "message": "Contém um ou mais dos seguintes carateres especiais $CHARS$", "placeholders": { "chars": { "content": "$1", @@ -6550,7 +6577,7 @@ "message": "obrigatório" }, "charactersCurrentAndMaximum": { - "message": "$CURRENT$/$MAX$ máximo de caracteres", + "message": "$CURRENT$/$MAX$ máximo de carateres", "placeholders": { "current": { "content": "$1", @@ -6563,7 +6590,7 @@ } }, "characterMaximum": { - "message": "Máximo de $MAX$ caracteres", + "message": "Máximo de $MAX$ carateres", "placeholders": { "max": { "content": "$1", @@ -6684,7 +6711,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Utilize $RECOMMENDED$ caracteres ou mais para gerar uma palavra-passe forte.", + "message": " Utilize $RECOMMENDED$ carateres ou mais para gerar uma palavra-passe forte.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Utilize a caixa de entrada de captura geral configurada para o seu domínio." }, + "useThisEmail": { + "message": "Utilizar este e-mail" + }, "random": { "message": "Aleatório", "description": "Generates domain-based username using random letters" @@ -7014,7 +7044,7 @@ "message": "O campo não é um endereço de e-mail." }, "inputMinLength": { - "message": "O campo deve ter pelo menos $COUNT$ caracteres.", + "message": "O campo deve ter pelo menos $COUNT$ carateres.", "placeholders": { "count": { "content": "$1", @@ -7023,7 +7053,7 @@ } }, "inputMaxLength": { - "message": "O campo não pode exceder os $COUNT$ caracteres de comprimento.", + "message": "O campo não pode exceder os $COUNT$ carateres de comprimento.", "placeholders": { "count": { "content": "$1", @@ -7032,7 +7062,7 @@ } }, "inputForbiddenCharacters": { - "message": "Não são permitidos os seguintes caracteres: $CHARACTERS$", + "message": "Não são permitidos os seguintes carateres: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -7041,7 +7071,7 @@ } }, "inputMinValue": { - "message": "O valor do campo tem de ser, pelo menos, $MIN$ caracteres.", + "message": "O valor do campo tem de ser, pelo menos, $MIN$ carateres.", "placeholders": { "min": { "content": "$1", @@ -7050,7 +7080,7 @@ } }, "inputMaxValue": { - "message": "O valor do campo não pode exceder os $MAX$ caracteres.", + "message": "O valor do campo não pode exceder os $MAX$ carateres.", "placeholders": { "max": { "content": "$1", @@ -7182,11 +7212,11 @@ "message": "Limpar tudo" }, "toggleCharacterCount": { - "message": "Mostrar/ocultar contagem de caracteres", + "message": "Mostrar/ocultar contagem de carateres", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, "passwordCharacterCount": { - "message": "Contagem de caracteres da palavra-passe", + "message": "Contagem de carateres da palavra-passe", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, "hide": { @@ -8190,7 +8220,7 @@ "message": "Palavra-passe fraca identificada e encontrada numa violação de dados. Utilize uma palavra-passe forte e única para proteger a sua conta. Tem a certeza de que pretende utilizar esta palavra-passe?" }, "characterMinimum": { - "message": "$LENGTH$ caracteres no mínimo", + "message": "$LENGTH$ carateres no mínimo", "placeholders": { "length": { "content": "$1", @@ -8199,7 +8229,7 @@ } }, "masterPasswordMinimumlength": { - "message": "A palavra-passe mestra deve ter pelo menos $LENGTH$ caracteres.", + "message": "A palavra-passe mestra deve ter pelo menos $LENGTH$ carateres.", "placeholders": { "length": { "content": "$1", @@ -8462,7 +8492,7 @@ } }, "next": { - "message": "Avançar" + "message": "Seguinte" }, "ssoLoginIsRequired": { "message": "É necessário um início de sessão SSO" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limitar a eliminação de coleções aos proprietários e administradores" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Os proprietários e administradores podem gerir todas as coleções e itens" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "mês por membro" }, + "monthPerMemberBilledAnnually": { + "message": "mês por membro faturado anualmente" + }, "seats": { "message": "Lugares" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Algoritmo de chaves" }, + "sshPrivateKey": { + "message": "Chave privada" + }, + "sshPublicKey": { + "message": "Chave pública" + }, + "sshFingerprint": { + "message": "Impressão digital" + }, "sshKeyFingerprint": { "message": "Impressão digital" }, @@ -9873,10 +9918,10 @@ "description": "The text, 'API', is an acronym and should not be translated." }, "showCharacterCount": { - "message": "Mostrar contagem de caracteres" + "message": "Mostrar contagem de carateres" }, "hideCharacterCount": { - "message": "Ocultar contagem de caracteres" + "message": "Ocultar contagem de carateres" }, "editAccess": { "message": "Editar acesso" @@ -9897,7 +9942,7 @@ "message": "Introduza o ID do HTML, o nome, a aria-label ou o placeholder do campo." }, "uppercaseDescription": { - "message": "Incluir caracteres em maiúsculas", + "message": "Incluir carateres em maiúsculas", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -9905,7 +9950,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Incluir caracteres em minúsculas", + "message": "Incluir carateres em minúsculas", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -9921,7 +9966,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Incluir caracteres especiais", + "message": "Incluir carateres especiais", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -10177,7 +10222,7 @@ "message": "Domínio reivindicado" }, "organizationNameMaxLength": { - "message": "O nome da organização não pode exceder 50 caracteres." + "message": "O nome da organização não pode exceder 50 carateres." }, "resellerRenewalWarningMsg": { "message": "A sua subscrição será renovada em breve. Para garantir um serviço ininterrupto, contacte a $RESELLER$ para confirmar a sua renovação antes de $RENEWAL_DATE$.", diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index a8a682b5cd4..966ef8fdcb3 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Ce fel de articol este acesta?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Zona de pericol" }, - "dangerZoneDesc": { - "message": "Atenție, aceste acțiuni nu sunt reversibile!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Revocare sesiuni" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Procedând astfel, veți fi deconectat din sesiunea curentă, fiind necesar să vă reconectați. De asemenea, vi se va solicita din nou autentificarea în două etape, dacă este configurată. Sesiunile active de pe alte dispozitive pot rămâne active timp de până la o oră." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Toate sesiunile au fost dezautorizate" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Utilizați inbox-ul catch-all configurat pentru domeniul dvs." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Aleatoriu", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index e50556a8309..622fe2f2705 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Всего приложений" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Выберите тип элемента" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Подтвердите вашу личность" }, + "weDontRecognizeThisDevice": { + "message": "Мы не распознали это устройство. Введите код, отправленный на ваш email, чтобы подтвердить вашу личность." + }, + "continueLoggingIn": { + "message": "Продолжить вход" + }, "whatIsADevice": { "message": "Что такое устройство?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Зона риска" }, - "dangerZoneDesc": { - "message": "Осторожно, эти действия необратимы!" - }, - "dangerZoneDescSingular": { - "message": "Будьте внимательны - это действие не обратимо!" - }, "deauthorizeSessions": { "message": "Деавторизовать сессии" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "В случае продолжения, ваша сессия будет завершена и вам будет предложено авторизоваться повторно. Вам также будет предложено выполнить двухэтапную аутентификацию, если она настроена. Сессии на других устройствах могут оставаться активными в течение одного часа." }, + "newDeviceLoginProtection": { + "message": "Авторизация с нового устройства" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Отключить защиту авторизации с нового устройства" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Включить защиту авторизации с нового устройства" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Продолжите, чтобы отключить письма от bitwarden отправляемые при авторизации с нового устройства." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Продолжите, чтобы включить письма от bitwarden отправляемые при авторизации с нового устройства." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Если защита авторизации с нового устройства отключена, любой с вашим мастер-паролем может получить доступ к вашему аккаунту с любого устройства. Чтобы обезопасить аккаунт при отключении писем от bitwarden настройте двухэтапную аутентификацию." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Изменения защиты авторизации сохранены" + }, "sessionsDeauthorized": { "message": "Все сессии деавторизованы" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Использовать общий email домена." }, + "useThisEmail": { + "message": "Использовать этот email" + }, "random": { "message": "Случайно", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Ограничить удаление коллекций владельцам и администраторам" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Владельцы и администраторы могут управлять всеми коллекциями и элементами" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "в месяц за пользователя" }, + "monthPerMemberBilledAnnually": { + "message": "месяц за пользователя в год" + }, "seats": { "message": "Места" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Алгоритм ключа" }, + "sshPrivateKey": { + "message": "Приватный ключ" + }, + "sshPublicKey": { + "message": "Публичный ключ" + }, + "sshFingerprint": { + "message": "Отпечаток" + }, "sshKeyFingerprint": { "message": "Отпечаток" }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index a3453b43c74..e30b09f9329 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 027f732874e..ffc9001562e 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Všetkých aplikácii" }, + "unmarkAsCriticalApp": { + "message": "Zrušiť označenie aplikácie za kritickú" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Označenie aplikácie za kritickú úspešne zrušené" + }, "whatTypeOfItem": { "message": "Aký typ položky to je?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Overte svoju totožnosť" }, + "weDontRecognizeThisDevice": { + "message": "Nespoznávame toto zariadenie. Pre overenie vašej identity zadajte kód ktorý bol zaslaný na váš email." + }, + "continueLoggingIn": { + "message": "Pokračovať v prihlasovaní" + }, "whatIsADevice": { "message": "Čo je zariadenie?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Riziková zóna" }, - "dangerZoneDesc": { - "message": "Opatrne, tieto zmeny nemožno vrátiť späť!" - }, - "dangerZoneDescSingular": { - "message": "Opatrne, túto zmenu nemožno vrátiť späť!" - }, "deauthorizeSessions": { "message": "Odhlásiť sedenia" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Ak budete pokračovať, budete tiež odhlásený z vášho súčasného sedenia a budete sa musieť opäť prihlásiť. Tiež budete opäť požiadaný o dvojstupňové prihlásenie ak ho máte zapnuté. Aktívne sedenia na iných zariadeniach môžu ostať aktívne až po dobu jednej hodiny." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Všetky sedenia odhlásené" }, @@ -3769,7 +3796,7 @@ } }, "unlinkedSso": { - "message": "Unlinked SSO." + "message": "Odpojené SSO." }, "unlinkedSsoUser": { "message": "SSO odpojené pre používateľa $ID$.", @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Použiť doručenú poštu typu catch-all nastavenú na doméne." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Náhodné", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Obmedziť vymazávanie zbierky len pre vlastníkov a administrátorov" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Vlastníci a správcovia môžu spravovať všetky zbierky a položky" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "mesačne za člena účtovaného ročne" + }, "seats": { "message": "Sedenia" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Algoritmus kľúča" }, + "sshPrivateKey": { + "message": "Súkromný kľúč" + }, + "sshPublicKey": { + "message": "Verejný kľúč" + }, + "sshFingerprint": { + "message": "Odtlačok" + }, "sshKeyFingerprint": { "message": "Odtlačok" }, diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 3325831a206..aec4c8bf0f4 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -1,18 +1,18 @@ { "allApplications": { - "message": "All applications" + "message": "Vse aplikacije" }, "criticalApplications": { - "message": "Critical applications" + "message": "Kritične aplikacije" }, "accessIntelligence": { - "message": "Access Intelligence" + "message": "Analiza dostopa" }, "riskInsights": { - "message": "Risk Insights" + "message": "Vpogled v tveganja" }, "passwordRisk": { - "message": "Password Risk" + "message": "Varnostno tveganje gesla" }, "reviewAtRiskPasswords": { "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Katere vrste element je to?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Območje nevarnosti" }, - "dangerZoneDesc": { - "message": "Previdno, ta dejanja so nepovratna!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Prekini seje" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Vse seje prekinjene" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Uporabite naslov za vse (\"catch-all\"), ki ste ga nastavili za svojo domeno." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index a3d8ac72406..2822da7ddee 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -15,7 +15,7 @@ "message": "Ризик од лозинке" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Прегледај ризичне лозинке (слабе, изложене или поново коришћене) у апликацијама. Изабери своје најкритичније апликације да би дао приоритет безбедносним радњама како би твоји корисници адресирали ризичне лозинке." }, "dataLastUpdated": { "message": "Подаци су последњи пут ажурирани: $DATE$", @@ -114,7 +114,7 @@ "message": "Чланови под ризиком" }, "atRiskMembersWithCount": { - "message": "At-risk members ($COUNT$)", + "message": "Ризични чланови ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -123,7 +123,7 @@ } }, "atRiskApplicationsWithCount": { - "message": "At-risk applications ($COUNT$)", + "message": "Ризичне апликације ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -132,13 +132,13 @@ } }, "atRiskMembersDescription": { - "message": "These members are logging into applications with weak, exposed, or reused passwords." + "message": "Ови чланови се пријављују у апликације са слабим, откривеним или поново коришћеним лозинкама." }, "atRiskApplicationsDescription": { - "message": "These applications have weak, exposed, or reused passwords." + "message": "Ове апликације имају слабу, проваљену или често коришћену лозинку." }, "atRiskMembersDescriptionWithApp": { - "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "message": "Ови чланови се пријављују у $APPNAME$ са слабим, откривеним или поново коришћеним лозинкама.", "placeholders": { "appname": { "content": "$1", @@ -155,6 +155,12 @@ "totalApplications": { "message": "Укупно апликација" }, + "unmarkAsCriticalApp": { + "message": "Уклони ознаку критичне апликације" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Успешно уклоњена ознака са критичне апликације" + }, "whatTypeOfItem": { "message": "Који је ово тип елемента?" }, @@ -810,7 +816,7 @@ "message": "Копирати телефон" }, "copyEmail": { - "message": "Копирати имејл" + "message": "Копирај Е-пошту" }, "copyCompany": { "message": "Копирати фирму" @@ -1161,11 +1167,17 @@ "verifyIdentity": { "message": "Потврдите идентитет" }, + "weDontRecognizeThisDevice": { + "message": "Не препознајемо овај уређај. Унесите код послат на адресу ваше електронске поште да би сте потврдили ваш идентитет." + }, + "continueLoggingIn": { + "message": "Настави са пријављивањем" + }, "whatIsADevice": { - "message": "What is a device?" + "message": "Шта је уређај?" }, "aDeviceIs": { - "message": "A device is a unique installation of the Bitwarden app where you have logged in. Reinstalling, clearing app data, or clearing your cookies could result in a device appearing multiple times." + "message": "Уређај је јединствена инсталација Bitwarden апликације на коју сте се пријавили. Поновно инсталирање, брисање података апликације или брисање колачића може довести до тога да се уређај појави више пута." }, "logInInitiated": { "message": "Пријава је покренута" @@ -1174,7 +1186,7 @@ "message": "Пошаљи" }, "emailAddressDesc": { - "message": "Користите ваш имејл за пријављивање." + "message": "Користи твоју Е-пошту за пријављивање." }, "yourName": { "message": "Ваше име" @@ -1207,7 +1219,7 @@ "message": "Савет Главне Лозинке" }, "masterPassHintText": { - "message": "Ако заборавите лозинку, наговештај за лозинку се може послати на ваш имејл. $CURRENT$/$MAXIMUM$ карактера максимум.", + "message": "Ако заборавиш лозинку, наговештај за лозинку се може послати на твоју Е-пошту. $CURRENT$/$MAXIMUM$ карактера максимум.", "placeholders": { "current": { "content": "$1", @@ -1223,7 +1235,7 @@ "message": "Подешавања" }, "accountEmail": { - "message": "Имејл налога" + "message": "Е-пошта налога" }, "requestHint": { "message": "Захтевај савет" @@ -1232,22 +1244,22 @@ "message": "Затражити савет лозинке" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Унесите имејл свог налога и биће вам послат савет за лозинку" + "message": "Унеси адресу Е-поште свог налога и биће ти послат савет за лозинку" }, "passwordHint": { "message": "Помоћ за лозинку" }, "enterEmailToGetHint": { - "message": "Унесите Ваш имејл да би добили савет за Вашу Главну Лозинку." + "message": "Унеси твоју Е-пошту да би добио савет за твоју Главну Лозинку." }, "getMasterPasswordHint": { "message": "Добити савет за Главну Лозинку" }, "emailRequired": { - "message": "Имејл је неопходан." + "message": "Адреса Е-поште је неопходна." }, "invalidEmail": { - "message": "Неисправан имејл." + "message": "Нетачна адреса Е-поште." }, "masterPasswordRequired": { "message": "Главна Лозинка је неопходна." @@ -1290,7 +1302,7 @@ "message": "Изаберите датум истека који је у будућности." }, "emailAddress": { - "message": "Имејл" + "message": "Адреса Е-поште" }, "yourVaultIsLockedV2": { "message": "Ваш сеф је блокиран" @@ -1387,7 +1399,7 @@ } }, "verificationCodeEmailSent": { - "message": "Провера имејла послата на $EMAIL$.", + "message": "Е-пошта за верификацију је послата на $EMAIL$.", "placeholders": { "email": { "content": "$1", @@ -1399,7 +1411,7 @@ "message": "Запамти ме" }, "sendVerificationCodeEmailAgain": { - "message": "Поново послати верификациони код на имејл" + "message": "Поново послати верификациони код на Е-пошту" }, "useAnotherTwoStepMethod": { "message": "Користите другу методу пријављивања у два корака" @@ -1755,10 +1767,10 @@ "message": "Молимо да се поново пријавите." }, "currentSession": { - "message": "Current session" + "message": "Тренутна сесија" }, "requestPending": { - "message": "Request pending" + "message": "Захтев је на чекању" }, "logBackInOthersToo": { "message": "Молимо вас да се поново пријавите. Ако користите друге Bitwarden апликације, одјавите се и вратите се и на њих." @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Опасна зона" }, - "dangerZoneDesc": { - "message": "Пажљиво, ове акције су крајне!" - }, - "dangerZoneDescSingular": { - "message": "Пажљиво, ова акција није реверзибилна!" - }, "deauthorizeSessions": { "message": "Одузели овлашћење сесије" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Наставак ће вас такође одјавити из тренутне сесије, што захтева поновно пријављивање. Од вас ће такође бити затражено да се поново пријавите у два корака, ако је омогућено. Активне сесије на другим уређајима могу да остану активне још један сат." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Одузето овлашћење свих сесија" }, @@ -3312,10 +3339,10 @@ } }, "inviteSingleEmailDesc": { - "message": "You have 1 invite remaining." + "message": "Преостала вам је 1 позивница." }, "inviteZeroEmailDesc": { - "message": "You have 0 invites remaining." + "message": "Преостало вам је 0 позивница." }, "userUsingTwoStep": { "message": "Овај корисник користи пријаву у два корака за заштиту свог налога." @@ -3769,7 +3796,7 @@ } }, "unlinkedSso": { - "message": "Unlinked SSO." + "message": "Неповезан SSO." }, "unlinkedSsoUser": { "message": "Отповезај SSO за $ID$.", @@ -3820,22 +3847,22 @@ "message": "Уређај" }, "loginStatus": { - "message": "Login status" + "message": "Статус пријаве" }, "firstLogin": { - "message": "First login" + "message": "Прва пријава" }, "trusted": { - "message": "Trusted" + "message": "Поуздан" }, "needsApproval": { - "message": "Needs approval" + "message": "Потребно је одобрење" }, "areYouTryingtoLogin": { - "message": "Are you trying to log in?" + "message": "Да ли покушавате да се пријавите?" }, "logInAttemptBy": { - "message": "Login attempt by $EMAIL$", + "message": "Покушај пријаве од $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -3844,22 +3871,22 @@ } }, "deviceType": { - "message": "Device Type" + "message": "Тип уређаја" }, "ipAddress": { - "message": "IP Address" + "message": "ИП адреса" }, "confirmLogIn": { - "message": "Confirm login" + "message": "Потврди пријављивање" }, "denyLogIn": { - "message": "Deny login" + "message": "Одбиј пријављивање" }, "thisRequestIsNoLongerValid": { - "message": "This request is no longer valid." + "message": "Овај захтев више није важећи." }, "logInConfirmedForEmailOnDevice": { - "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "message": "Пријава потврђена за $EMAIL$ на $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -3872,16 +3899,16 @@ } }, "youDeniedALogInAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + "message": "Одбили сте покушај пријаве са другог уређаја. Ако сте то заиста били ви, покушајте поново да се пријавите помоћу уређаја." }, "loginRequestHasAlreadyExpired": { - "message": "Login request has already expired." + "message": "Захтев за пријаву је већ истекао." }, "justNow": { - "message": "Just now" + "message": "Управо сада" }, "requestedXMinutesAgo": { - "message": "Requested $MINUTES$ minutes ago", + "message": "Затражено пре $MINUTES$ минута", "placeholders": { "minutes": { "content": "$1", @@ -4007,7 +4034,7 @@ "message": "Ажурирајте Претраживач" }, "generatingRiskInsights": { - "message": "Generating your risk insights..." + "message": "Генерисање прегледа вашег ризика..." }, "updateBrowserDesc": { "message": "Користите неподржани веб прегледач. Веб сеф можда неће правилно функционисати." @@ -4849,7 +4876,7 @@ "message": "Пријавите се помоћу портала за јединствену пријаву ваше организације. Унесите идентификатор организације да бисте започели." }, "singleSignOnEnterOrgIdentifier": { - "message": "Enter your organization's SSO identifier to begin" + "message": "За почетак унесите SSO идентификатор ваше организације" }, "singleSignOnEnterOrgIdentifierText": { "message": "To log in with your SSO provider, enter your organization's SSO identifier to begin. You may need to enter this SSO identifier when you log in from a new device." @@ -5780,17 +5807,17 @@ "message": "Грешка" }, "decryptionError": { - "message": "Decryption error" + "message": "Грешка при декрипцији" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden није могао да декриптује ставке из трезора наведене испод." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Обратите се корисничкој подршци", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "да бисте избегли додатни губитак података.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "accountRecoveryManageUsers": { @@ -6124,10 +6151,10 @@ "message": "Assertion consumer service (ACS) URL" }, "spNameIdFormat": { - "message": "Name ID format" + "message": "Формат ИД назива" }, "spOutboundSigningAlgorithm": { - "message": "Outbound signing algorithm" + "message": "Алгоритам за излазно потписивање" }, "spSigningBehavior": { "message": "Понашање пријављања" @@ -6169,7 +6196,7 @@ "message": "Дозволи нежељени одговор на аутентификацију" }, "idpAllowOutboundLogoutRequests": { - "message": "Allow outbound logout requests" + "message": "Дозволи одлазне захтеве за одјављивањем" }, "idpSignAuthenticationRequests": { "message": "Sign authentication requests" @@ -6265,7 +6292,7 @@ } }, "freeFamiliesPlan": { - "message": "Free Families plan" + "message": "Бесплатан породични план" }, "redeemNow": { "message": "Откупи сада" @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Случајно", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Ограничите брисање збирке на власнике и администраторе" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Власници и администратори могу да управљају свим колекцијама и ставкама" }, @@ -9196,19 +9229,19 @@ "message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider." }, "eventManagement": { - "message": "Event management" + "message": "Управљање догађајима" }, "eventManagementDesc": { "message": "Integrate Bitwarden event logs with your SIEM (system information and event management) system by using the implementation guide for your platform." }, "deviceManagement": { - "message": "Device management" + "message": "Управљање уређајима" }, "deviceManagementDesc": { - "message": "Configure device management for Bitwarden using the implementation guide for your platform." + "message": "Конфигуришите управљање уређајима за Bitwarden помоћу водича за имплементацију за своју платформу." }, "integrationCardTooltip": { - "message": "Launch $INTEGRATION$ implementation guide.", + "message": "Покренути $INTEGRATION$ водич за имплементацију.", "placeholders": { "integration": { "content": "$1", @@ -9217,7 +9250,7 @@ } }, "smIntegrationTooltip": { - "message": "Set up $INTEGRATION$.", + "message": "Подесити $INTEGRATION$.", "placeholders": { "integration": { "content": "$1", @@ -9226,7 +9259,7 @@ } }, "smSdkTooltip": { - "message": "View $SDK$ repository", + "message": "Преглед $SDK$ спремишта", "placeholders": { "sdk": { "content": "$1", @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "месечно по члану" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Места" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Алгоритам кључа" }, + "sshPrivateKey": { + "message": "Приватни кључ" + }, + "sshPublicKey": { + "message": "Јавни кључ" + }, + "sshFingerprint": { + "message": "Отисак" + }, "sshKeyFingerprint": { "message": "Отисак прста" }, @@ -9866,7 +9911,7 @@ "message": "Ваша комплементарна једногодишња претплата на Менаџер Лозинки ће надоградити на изабрани план. Неће вам бити наплаћено док се бесплатни период не заврши." }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "Датотека је сачувана на уређају. Управљајте преузимањима са свог уређаја." }, "publicApi": { "message": "Јавни API", @@ -10044,7 +10089,7 @@ "message": "We have made a micro-deposit to your bank account (this may take 1-2 business days). Enter the six-digit code starting with 'SM' found on the deposit description. Failure to verify the bank account will result in a missed payment and your subscription being suspended." }, "descriptorCode": { - "message": "Descriptor code" + "message": "Кôд дескриптора" }, "importantNotice": { "message": "Важно обавештење" @@ -10086,13 +10131,13 @@ "message": "Уклони чланове" }, "devices": { - "message": "Devices" + "message": "Уређаји" }, "deviceListDescription": { - "message": "Your account was logged in to each of the devices below. If you do not recognize a device, remove it now." + "message": "Ваш рачун је пријављен на сваку од доле наведених уређаја. Ако не препознајете уређај, извадите га сада." }, "deviceListDescriptionTemp": { - "message": "Your account was logged in to each of the devices below." + "message": "Ваш рачун је пријављен на сваку од доле наведених уређаја." }, "claimedDomains": { "message": "Claimed domains" @@ -10226,10 +10271,10 @@ "message": "Organization subscription restarted" }, "restartSubscription": { - "message": "Restart your subscription" + "message": "Поново покрените претплату" }, "suspendedManagedOrgMessage": { - "message": "Contact $PROVIDER$ for assistance.", + "message": "Контактирајте $PROVIDER$ за помоћ.", "placeholders": { "provider": { "content": "$1", diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 2e57f7b3523..bcac281eaeb 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Kog je tipa ovaj unos?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Opasna zona" }, - "dangerZoneDesc": { - "message": "Pažljivo, ove odluke se ne mogu poništiti!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 8a93d6cb0e3..b33e455bb9c 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Vilken typ av objekt är detta?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Farozon" }, - "dangerZoneDesc": { - "message": "Var försiktig, dessa åtgärder går inte att ångra!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Avauktorisera sessioner" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Om du fortsätter kommer du loggas ut från sin nuvarande session vilket kräver att du loggar in igen. Du kommer även behöva verifiera med tvåstegsverifiering om det är aktiverat. Aktiva sessioner på andra enheter kan fortsätta vara aktiva i upp till en timme." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Alla sessioner avauktoriserades" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Slumpmässigt", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Ägare och administratörer kan hantera alla samlingar och objekt" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "månad per medlem" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Nyckelalgoritm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingeravtryck" }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index bb7d3b657b5..3b8d147b95f 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 9003b4e2570..8136d1f9cc9 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "What type of item is this?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Verify your Identity" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Danger zone" }, - "dangerZoneDesc": { - "message": "Careful, these actions are not reversible!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Deauthorize sessions" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "All sessions deauthorized" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Owners and admins can manage all collections and items" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 57078576569..0ea5e52fd3d 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Toplam uygulama" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Bu kaydın türü nedir?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Kimliğinizi doğrulayın" }, + "weDontRecognizeThisDevice": { + "message": "Bu cihazı tanıyamadık. Kimliğinizi doğrulamak için e-postanıza gönderilen kodu girin." + }, + "continueLoggingIn": { + "message": "Giriş yapmaya devam et" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Tehlikeli bölge" }, - "dangerZoneDesc": { - "message": "Dikkatli olun, bu işlemleri geri alamazsınız!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Oturumları kapat" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Devam ederseniz geçerli oturumunuz da sonlanacak ve yeniden oturum açmanız gerekecek. İki aşamalı girişi etkinleştirdiyseniz onu da tamamlamanız gerekecek. Diğer cihazlardaki aktif oturumlar bir saate kadar aktif kalabilir." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Tüm oturumlar kapatıldı" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Alan adınızın tüm iletileri yakalamaya ayarlanmış adresini kullanın." }, + "useThisEmail": { + "message": "Bu e-postayı kullan" + }, "random": { "message": "Rastgele", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Koleksiyon silmeyi sahipler ve yöneticilerle sınırlandırın" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Sahipler ve yöneticiler tüm koleksiyonları ve öğeleri yönetebilir" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Özel anahtar" + }, + "sshPublicKey": { + "message": "Ortak anahtar" + }, + "sshFingerprint": { + "message": "Parmak izi" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 5e72f2007cd..07a8f8893cd 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -114,7 +114,7 @@ "message": "Ризиковані учасники" }, "atRiskMembersWithCount": { - "message": "At-risk members ($COUNT$)", + "message": "Учасники з ризиком ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -123,7 +123,7 @@ } }, "atRiskApplicationsWithCount": { - "message": "At-risk applications ($COUNT$)", + "message": "Програми з ризиком ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -132,13 +132,13 @@ } }, "atRiskMembersDescription": { - "message": "These members are logging into applications with weak, exposed, or reused passwords." + "message": "Ці учасники використовують у програмах слабкі, викриті, або повторювані паролі." }, "atRiskApplicationsDescription": { - "message": "These applications have weak, exposed, or reused passwords." + "message": "Ці програми мають слабкі, викриті, або повторювані паролі." }, "atRiskMembersDescriptionWithApp": { - "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", + "message": "Ці учасники використовують у $APPNAME$ слабкі, викриті, або повторювані паролі.", "placeholders": { "appname": { "content": "$1", @@ -155,6 +155,12 @@ "totalApplications": { "message": "Всього програм" }, + "unmarkAsCriticalApp": { + "message": "Зняти позначку критичної програми" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Позначку критичної програми знято" + }, "whatTypeOfItem": { "message": "Який це тип запису?" }, @@ -1161,11 +1167,17 @@ "verifyIdentity": { "message": "Підтвердьте свою особу" }, + "weDontRecognizeThisDevice": { + "message": "Ми не розпізнаємо цей пристрій. Введіть код, надісланий на вашу електронну пошту, щоб підтвердити вашу особу." + }, + "continueLoggingIn": { + "message": "Продовжити вхід" + }, "whatIsADevice": { - "message": "What is a device?" + "message": "Що таке пристрій?" }, "aDeviceIs": { - "message": "A device is a unique installation of the Bitwarden app where you have logged in. Reinstalling, clearing app data, or clearing your cookies could result in a device appearing multiple times." + "message": "Пристрій – це унікальне встановлення програми Bitwarden, де ви увійшли в систему. Перевстановлення, очищення даних програми або очищення файлів cookie може призвести до того, що пристрій з'являтиметься кілька разів." }, "logInInitiated": { "message": "Ініційовано вхід" @@ -1755,10 +1767,10 @@ "message": "Повторно виконайте вхід." }, "currentSession": { - "message": "Current session" + "message": "Поточний сеанс" }, "requestPending": { - "message": "Request pending" + "message": "Запит в очікуванні" }, "logBackInOthersToo": { "message": "Будь ласка, повторно виконайте вхід. Якщо ви користуєтесь іншими програмами Bitwarden, також вийдіть із них, і знову увійдіть." @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Небезпечна зона" }, - "dangerZoneDesc": { - "message": "Обережно, ці дії неможливо скасувати!" - }, - "dangerZoneDescSingular": { - "message": "Обережно, цю дію неможливо скасувати!" - }, "deauthorizeSessions": { "message": "Завершити сеанси" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Продовжуючи, ви також вийдете з поточного сеансу і необхідно буде виконати вхід знову. Ви також отримаєте повторний запит двоетапної перевірки, якщо вона увімкнена. Активні сеанси на інших пристроях можуть залишатися активними протягом години." }, + "newDeviceLoginProtection": { + "message": "Вхід з нового пристрою" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Вимкнути захист входу з нового пристрою" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Увімкнути захист входу з нового пристрою" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Виконайте наведені нижче дії, щоб вимкнути надсилання підтверджень під час входу з нового пристрою." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Виконайте наведені нижче дії, щоб увімкнути надсилання підтверджень під час входу з нового пристрою." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "Якщо захист входу з нового пристрою вимкнено, будь-хто може отримати доступ до вашого облікового запису з будь-якого пристрою, знаючи головний пароль. Щоб захистити свій обліковий запис і не вмикати надсилання підтверджень, налаштуйте двоетапну перевірку." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "Зміни захисту входу з нового пристрою збережено" + }, "sessionsDeauthorized": { "message": "Усі сеанси завершено" }, @@ -3312,10 +3339,10 @@ } }, "inviteSingleEmailDesc": { - "message": "You have 1 invite remaining." + "message": "У вас залишилось 1 запрошення." }, "inviteZeroEmailDesc": { - "message": "You have 0 invites remaining." + "message": "У вас залишилося 0 запрошень." }, "userUsingTwoStep": { "message": "Цей користувач використовує двоетапну перевірку для захисту свого облікового запису." @@ -3769,7 +3796,7 @@ } }, "unlinkedSso": { - "message": "Unlinked SSO." + "message": "Від'єднаний SSO." }, "unlinkedSsoUser": { "message": "Користувач $ID$ не має пов'язаного SSO.", @@ -3820,22 +3847,22 @@ "message": "Пристрій" }, "loginStatus": { - "message": "Login status" + "message": "Стан входу в систему" }, "firstLogin": { - "message": "First login" + "message": "Перший вхід" }, "trusted": { - "message": "Trusted" + "message": "Надійний" }, "needsApproval": { - "message": "Needs approval" + "message": "Потребує підтвердження" }, "areYouTryingtoLogin": { - "message": "Are you trying to log in?" + "message": "Ви намагаєтесь увійти?" }, "logInAttemptBy": { - "message": "Login attempt by $EMAIL$", + "message": "Спроба входу з $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -3844,22 +3871,22 @@ } }, "deviceType": { - "message": "Device Type" + "message": "Тип пристрою" }, "ipAddress": { - "message": "IP Address" + "message": "IP-адреса" }, "confirmLogIn": { - "message": "Confirm login" + "message": "Підтвердити вхід" }, "denyLogIn": { - "message": "Deny login" + "message": "Заборонити вхід" }, "thisRequestIsNoLongerValid": { - "message": "This request is no longer valid." + "message": "Цей запит більше недійсний." }, "logInConfirmedForEmailOnDevice": { - "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "message": "Підтверджено вхід для $EMAIL$ на $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -3872,16 +3899,16 @@ } }, "youDeniedALogInAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + "message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були дійсно ви, спробуйте увійти з пристроєм знову." }, "loginRequestHasAlreadyExpired": { - "message": "Login request has already expired." + "message": "Термін дії запиту на вхід завершився." }, "justNow": { - "message": "Just now" + "message": "Щойно" }, "requestedXMinutesAgo": { - "message": "Requested $MINUTES$ minutes ago", + "message": "Запитано $MINUTES$ хвилин тому", "placeholders": { "minutes": { "content": "$1", @@ -5780,17 +5807,17 @@ "message": "Помилка" }, "decryptionError": { - "message": "Decryption error" + "message": "Помилка розшифрування" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden не зміг розшифрувати вказані нижче елементи сховища." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Зверніться до служби підтримки клієнтів,", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "щоб уникнути втрати даних.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "accountRecoveryManageUsers": { @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Використовуйте свою скриньку вхідних Catch-All власного домену." }, + "useThisEmail": { + "message": "Використати цю е-пошту" + }, "random": { "message": "Випадково", "description": "Generates domain-based username using random letters" @@ -8272,31 +8302,31 @@ "message": "Довірені пристрої" }, "memberDecryptionOptionTdeDescPart1": { - "message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The", + "message": "Учасникам не потрібен головний пароль під час входу з використанням SSO. Натомість використовується ключ шифрування, що зберігається на пристрої, роблячи цей пристрій довіреним. Перший пристрій, на якому учасник реєструється і входить до системи, буде довіреним. Нові пристрої необхідно схвалити за допомогою наявного довіреного пристрою або адміністратором.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescLink1": { - "message": "single organization", + "message": "Політику єдиної", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescPart2": { - "message": "policy,", + "message": "організації,", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescLink2": { - "message": "SSO required", + "message": "політику обов'язкового", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescPart3": { - "message": "policy, and", + "message": "SSO та", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescLink3": { - "message": "account recovery administration", + "message": "політику адміністрування облікового", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescPart4": { - "message": "policy will turn on when this option is used.", + "message": "запису буде ввімкнено, якщо використовується цей параметр.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { @@ -8375,16 +8405,16 @@ "message": "Схвалити запит" }, "deviceApproved": { - "message": "Device approved" + "message": "Пристрій схвалено" }, "deviceRemoved": { - "message": "Device removed" + "message": "Пристрій вилучено" }, "removeDevice": { - "message": "Remove device" + "message": "Вилучити пристрій" }, "removeDeviceConfirmation": { - "message": "Are you sure you want to remove this device?" + "message": "Ви дійсно хочете вилучити цей пристрій?" }, "noDeviceRequests": { "message": "Немає запитів з пристрою" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Дозволити видалення збірок лише власникам та адміністраторам" }, + "limitItemDeletionDesc": { + "message": "Обмежити видалення записів для учасників, які мають дозвіл \"Може керувати\"" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Власники та адміністратори можуть керувати всіма збірками та записами" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "на місяць за учасника" }, + "monthPerMemberBilledAnnually": { + "message": "місяць за учасника сплачується щороку" + }, "seats": { "message": "Місця" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Алгоритм ключа" }, + "sshPrivateKey": { + "message": "Закритий ключ" + }, + "sshPublicKey": { + "message": "Відкритий ключ" + }, + "sshFingerprint": { + "message": "Цифровий відбиток" + }, "sshKeyFingerprint": { "message": "Цифровий відбиток" }, @@ -10086,13 +10131,13 @@ "message": "Вилучити учасників" }, "devices": { - "message": "Devices" + "message": "Пристрої" }, "deviceListDescription": { - "message": "Your account was logged in to each of the devices below. If you do not recognize a device, remove it now." + "message": "Вхід у ваш обліковий запис виконано на пристроях, зазначених нижче. Якщо ви не розпізнаєте пристрій, вилучіть його." }, "deviceListDescriptionTemp": { - "message": "Your account was logged in to each of the devices below." + "message": "Вхід у ваш обліковий запис виконано на пристроях, зазначених нижче." }, "claimedDomains": { "message": "Заявлені домени" @@ -10180,7 +10225,7 @@ "message": "Назва організації не може перевищувати 50 символів." }, "resellerRenewalWarningMsg": { - "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", + "message": "Ваша передплата невдовзі поновиться. Щоб забезпечити безперебійну роботу, зверніться до $RESELLER$ для підтвердження поновлення до $RENEWAL_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10193,7 +10238,7 @@ } }, "resellerOpenInvoiceWarningMgs": { - "message": "An invoice for your subscription was issued on $ISSUED_DATE$. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.", + "message": "Рахунок за вашу передплату випущено $ISSUED_DATE$. Щоб забезпечити безперебійну роботу, зверніться до $RESELLER$ для підтвердження поновлення до $DUE_DATE$.", "placeholders": { "reseller": { "content": "$1", @@ -10210,7 +10255,7 @@ } }, "resellerPastDueWarningMsg": { - "message": "The invoice for your subscription has not been paid. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.", + "message": "Рахунок за вашу передплату ще не сплачено. Щоб забезпечити безперебійну роботу, зверніться до $RESELLER$ для підтвердження поновлення до $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -10223,13 +10268,13 @@ } }, "restartOrganizationSubscription": { - "message": "Organization subscription restarted" + "message": "Передплату організації розпочато повторно" }, "restartSubscription": { - "message": "Restart your subscription" + "message": "Розпочніть повторно свою передплату" }, "suspendedManagedOrgMessage": { - "message": "Contact $PROVIDER$ for assistance.", + "message": "Звернутися до $PROVIDER$ по допомогу.", "placeholders": { "provider": { "content": "$1", diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 17097c3d90c..834a8db5e7a 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "Mục này là gì?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "Xác minh danh tính của bạn" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "Vùng nguy hiểm" }, - "dangerZoneDesc": { - "message": "Cẩn thận, thao tác này không thể khôi phục!" - }, - "dangerZoneDescSingular": { - "message": "Careful, this action is not reversible!" - }, "deauthorizeSessions": { "message": "Gỡ phiên" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "Sẽ đăng xuất bạn ra khỏi phiên hiện tại, sau đó cần đăng nhập lại. Bạn cũng sẽ phải đăng nhập hai bước lại nếu bạn có đăng nhập hai bước. Những phiên đăng nhập trên các thiết bị khác sẽ tiếp tục có hiệu lực lên đến 1 tiếng." }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "Tất cả phiên đăng nhập đã bị gỡ" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "Random", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "Chủ sở hữu và quản trị viên có thể quản lý tất cả các bộ sưu tập và mục" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "Seats" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index e19d3965305..8167d2063b7 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -155,6 +155,12 @@ "totalApplications": { "message": "总的应用程序" }, + "unmarkAsCriticalApp": { + "message": "取消标记为关键应用程序" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "关键应用程序已成功取消标记" + }, "whatTypeOfItem": { "message": "这是什么类型的项目?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "验证您的身份" }, + "weDontRecognizeThisDevice": { + "message": "我们无法识别这个设备。请输入发送到您电子邮箱中的代码以验证您的身份。" + }, + "continueLoggingIn": { + "message": "继续登录" + }, "whatIsADevice": { "message": "什么是设备?" }, @@ -1186,7 +1198,7 @@ "message": "主密码" }, "masterPassDesc": { - "message": "主密码是您访问密码库的密码。它非常重要,请您不要忘记。一旦忘记,无任何办法恢复此密码。" + "message": "主密码是用于访问您的密码库的密码。不要忘记您的主密码,这一点非常重要。一旦忘记,无任何办法恢复此密码。" }, "masterPassImportant": { "message": "主密码忘记后,将无法恢复!" @@ -1351,7 +1363,7 @@ "message": "没有可列出的事件。" }, "newOrganization": { - "message": "新建组织" + "message": "新增组织" }, "noOrganizationsList": { "message": "您没有加入任何组织。同一组织的用户可以安全地与其他用户共享项目。" @@ -1456,7 +1468,7 @@ "message": "FIDO U2F 安全钥匙" }, "webAuthnTitle": { - "message": "FIDO2 WebAuthn" + "message": "通行密钥" }, "webAuthnDesc": { "message": "使用您设备的生物识别或 WebAuthn 兼容的安全钥匙。" @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "危险操作区" }, - "dangerZoneDesc": { - "message": "当心,这些操作无法撤销!" - }, - "dangerZoneDescSingular": { - "message": "当心,此操作无法撤销!" - }, "deauthorizeSessions": { "message": "取消会话授权" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "继续操作还将使您退出当前会话,并要求您重新登录。如果有设置两步登录,也需要重新验证。其他设备上的活动会话可能会继续保持活动状态长达一小时。" }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "已取消所有会话授权" }, @@ -2431,7 +2458,7 @@ "message": "发现暴露的密码" }, "exposedPasswordsFoundReportDesc": { - "message": "我们在您的 $VAULT$ 中发现了 $COUNT$ 个在已知的数据泄露事件中暴露了密码的项目。您应更改它们以使用新的密码。", + "message": "我们在您的 $VAULT$ 中发现了 $COUNT$ 个在已知的数据泄露中暴露了密码的项目。您应更改它们以使用新的密码。", "placeholders": { "count": { "content": "$1", @@ -2444,7 +2471,7 @@ } }, "noExposedPasswords": { - "message": "您的密码库中没有在已知数据泄露事件中被暴露密码的项目。" + "message": "您的密码库中没有在已知数据泄露中暴露了密码的项目。" }, "checkExposedPasswords": { "message": "检查暴露的密码" @@ -2752,7 +2779,7 @@ "message": "任何未付费订阅都将通过您的付款方式收取费用。" }, "paymentChargedWithTrial": { - "message": "您的计划包含了 7 天的免费试用期。在试用期结束前,不会从您的付款方式中扣款。您可以随时取消。" + "message": "您的计划包含了 7 天的免费试用。在试用期结束前,不会从您的付款方式中扣款。您可以随时取消。" }, "paymentInformation": { "message": "支付信息" @@ -3769,7 +3796,7 @@ } }, "unlinkedSso": { - "message": "Unlinked SSO." + "message": "未链接 SSO。" }, "unlinkedSsoUser": { "message": "为用户 $ID$ 取消链接 SSO。", @@ -3881,7 +3908,7 @@ "message": "刚刚" }, "requestedXMinutesAgo": { - "message": "$MINUTES$ 分钟前已发出请求", + "message": "$MINUTES$ 分钟前已请求", "placeholders": { "minutes": { "content": "$1", @@ -4558,7 +4585,7 @@ "description": "ex. A strong password. Scale: Very Weak -> Weak -> Good -> Strong" }, "good": { - "message": "良好", + "message": "良", "description": "ex. A good password. Scale: Very Weak -> Weak -> Good -> Strong" }, "weak": { @@ -5048,10 +5075,10 @@ "message": "确定移除此密码?" }, "hideEmail": { - "message": "对收件人隐藏我的电子邮箱地址。" + "message": "对接收者隐藏我的电子邮箱地址。" }, "disableThisSend": { - "message": "停用此 Send 则任何人无法访问它。", + "message": "停用此 Send 确保无人能访问它。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "allSends": { @@ -5076,7 +5103,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPasswordDontKnow": { - "message": "不知道密码?请向提供此 Send 的发件人索要密码。", + "message": "不知道密码吗?请向发送者索取访问此 Send 所需的密码。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendHiddenByDefault": { @@ -5282,14 +5309,14 @@ "message": "可以管理组织策略的组织成员豁免此策略。" }, "disableHideEmail": { - "message": "在创建或编辑 Send 时,始终向收件人显示成员的电子邮箱地址。", + "message": "创建或编辑 Send 时,始终向接收者显示成员的电子邮箱地址。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendOptionsPolicyInEffect": { "message": "以下组织策略目前正起作用:" }, "sendDisableHideEmailInEffect": { - "message": "创建或编辑 Send 时,不允许用户对收件人隐藏他们的电子邮箱地址。", + "message": "创建或编辑 Send 时,不允许用户对接收者隐藏他们的电子邮箱地址。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "modifiedPolicyId": { @@ -5400,10 +5427,10 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTextDesc": { - "message": "您想要发送的文本。" + "message": "您想在此 Send 中附加的文本。" }, "sendFileDesc": { - "message": "您想要发送的文件。" + "message": "您想在此 Send 中附加的文件。" }, "copySendLinkOnSave": { "message": "保存时复制链接到剪贴板以便分享此 Send。" @@ -5852,7 +5879,7 @@ "message": "提供商" }, "newClientOrganization": { - "message": "新建客户组织" + "message": "新增客户组织" }, "newClientOrganizationDesc": { "message": "创建一个新的客户组织,该组织将作为提供商与你关联。您将可以访问和管理这个组织。" @@ -6136,7 +6163,7 @@ "message": "最小入站签名算法" }, "spWantAssertionsSigned": { - "message": "要求使用签名的断言" + "message": "期望已签名的断言" }, "spValidateCertificates": { "message": "验证证书" @@ -6172,7 +6199,7 @@ "message": "允许出站注销请求" }, "idpSignAuthenticationRequests": { - "message": "签名身份验证请求" + "message": "签署身份验证请求" }, "ssoSettingsSaved": { "message": "单点登录配置已保存" @@ -6271,7 +6298,7 @@ "message": "立即兑换" }, "recipient": { - "message": "收件人" + "message": "接收者" }, "removeSponsorship": { "message": "移除赞助" @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "使用您的域名配置的 Catch-all 收件箱。" }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "随机", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "限制为仅所有者和管理员可以删除集合" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "所有者和管理员可以管理所有集合和项目" }, @@ -9271,7 +9304,10 @@ "message": "35% 折扣" }, "monthPerMember": { - "message": "每成员 /月" + "message": "月 /成员" + }, + "monthPerMemberBilledAnnually": { + "message": "月 /成员(按年计费)" }, "seats": { "message": "席位" @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "密钥算法" }, + "sshPrivateKey": { + "message": "私钥" + }, + "sshPublicKey": { + "message": "公钥" + }, + "sshFingerprint": { + "message": "指纹" + }, "sshKeyFingerprint": { "message": "指纹" }, diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 4bb67a786a9..9087f640fcf 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -1,6 +1,6 @@ { "allApplications": { - "message": "All applications" + "message": "所有應用程式" }, "criticalApplications": { "message": "重要應用程式" @@ -108,7 +108,7 @@ "message": "全部密碼" }, "searchApps": { - "message": "Search applications" + "message": "搜尋應用程式" }, "atRiskMembers": { "message": "具有風險的成員" @@ -123,7 +123,7 @@ } }, "atRiskApplicationsWithCount": { - "message": "At-risk applications ($COUNT$)", + "message": "具有風險的應用程式 ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -155,6 +155,12 @@ "totalApplications": { "message": "Total applications" }, + "unmarkAsCriticalApp": { + "message": "Unmark as critical app" + }, + "criticalApplicationSuccessfullyUnmarked": { + "message": "Critical application successfully unmarked" + }, "whatTypeOfItem": { "message": "這是什麼類型的項目?" }, @@ -1161,6 +1167,12 @@ "verifyIdentity": { "message": "核實你的身份" }, + "weDontRecognizeThisDevice": { + "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + }, + "continueLoggingIn": { + "message": "Continue logging in" + }, "whatIsADevice": { "message": "What is a device?" }, @@ -1827,12 +1839,6 @@ "dangerZone": { "message": "危險區域" }, - "dangerZoneDesc": { - "message": "小心,這些動作無法復原!" - }, - "dangerZoneDescSingular": { - "message": "小心,此操作無法復原!" - }, "deauthorizeSessions": { "message": "取消工作階段授權" }, @@ -1842,6 +1848,27 @@ "deauthorizeSessionsWarning": { "message": "接下來會登出目前的工作階段,並要求您重新登入。若您有設定兩步驟登入,也需重新驗證。其他裝置上的活動工作階段最多會保持一個小時。" }, + "newDeviceLoginProtection": { + "message": "New device login" + }, + "turnOffNewDeviceLoginProtection": { + "message": "Turn off new device login protection" + }, + "turnOnNewDeviceLoginProtection": { + "message": "Turn on new device login protection" + }, + "turnOffNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to turn off the verification emails bitwarden sends when you login from a new device." + }, + "turnOnNewDeviceLoginProtectionModalDesc": { + "message": "Proceed below to have bitwarden send you verification emails when you login from a new device." + }, + "turnOffNewDeviceLoginProtectionWarning": { + "message": "With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login." + }, + "accountNewDeviceLoginProtectionSaved": { + "message": "New device login protection changes saved" + }, "sessionsDeauthorized": { "message": "已取消所有工作階段授權" }, @@ -6719,6 +6746,9 @@ "catchallEmailDesc": { "message": "使用您的網域設定的 Catch-all 收件匣。" }, + "useThisEmail": { + "message": "Use this email" + }, "random": { "message": "隨機", "description": "Generates domain-based username using random letters" @@ -8595,6 +8625,9 @@ "limitCollectionDeletionDesc": { "message": "Limit collection deletion to owners and admins" }, + "limitItemDeletionDesc": { + "message": "Limit item deletion to members with the Can manage permission" + }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "擁有人與管理員可以管理所有分類與項目" }, @@ -9273,6 +9306,9 @@ "monthPerMember": { "message": "month per member" }, + "monthPerMemberBilledAnnually": { + "message": "month per member billed annually" + }, "seats": { "message": "席位" }, @@ -9793,6 +9829,15 @@ "sshKeyAlgorithm": { "message": "Key algorithm" }, + "sshPrivateKey": { + "message": "Private key" + }, + "sshPublicKey": { + "message": "Public key" + }, + "sshFingerprint": { + "message": "Fingerprint" + }, "sshKeyFingerprint": { "message": "Fingerprint" }, diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index 2c0108ca3e2..4a3f6cfef1b 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -5,7 +5,7 @@ config.content = [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", - "../../libs/key-management/src/**/*.{html,ts}", + "../../libs/key-management-ui/src/**/*.{html,ts}", "../../libs/vault/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}", "../../bitwarden_license/bit-web/src/**/*.{html,ts}", diff --git a/apps/web/tsconfig.build.json b/apps/web/tsconfig.build.json index b10c4f9d899..39ab37efbb8 100644 --- a/apps/web/tsconfig.build.json +++ b/apps/web/tsconfig.build.json @@ -1,5 +1,8 @@ { "extends": "./tsconfig.json", "files": ["src/polyfills.ts", "src/main.ts", "src/theme.ts"], - "include": ["src/connectors/*.ts", "../../libs/common/src/platform/services/**/*.worker.ts"] + "include": [ + "src/connectors/*.ts", + "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts" + ] } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 701808df132..c05f24b9a8d 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -18,10 +18,10 @@ "@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"], - "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/importer-core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/key-management": ["../../libs/key-management/src"], - "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], + "@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["../../libs/tools/card/src"], @@ -43,6 +43,6 @@ "src/connectors/*.ts", "src/**/*.stories.ts", "src/**/*.spec.ts", - "../../libs/common/src/platform/services/**/*.worker.ts" + "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts" ] } diff --git a/bitwarden_license/bit-cli/.eslintrc.json b/bitwarden_license/bit-cli/.eslintrc.json deleted file mode 100644 index 10d22388378..00000000000 --- a/bitwarden_license/bit-cli/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "node": true - } -} diff --git a/bitwarden_license/bit-cli/src/admin-console/.eslintrc.json b/bitwarden_license/bit-cli/src/admin-console/.eslintrc.json deleted file mode 100644 index 38467187294..00000000000 --- a/bitwarden_license/bit-cli/src/admin-console/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../../libs/admin-console/.eslintrc.json" -} diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts index db63a3c1c68..dc78ebf7cd0 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { MessageResponse } from "@bitwarden/cli/models/response/message.response"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -14,6 +16,7 @@ export class ApproveAllCommand { constructor( private organizationAuthRequestService: OrganizationAuthRequestService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} async run(organizationId: string): Promise { @@ -25,7 +28,13 @@ export class ApproveAllCommand { return Response.badRequest("`" + organizationId + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -58,6 +67,7 @@ export class ApproveAllCommand { return new ApproveAllCommand( serviceContainer.organizationAuthRequestService, serviceContainer.organizationService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts index 1c51f9397c5..27597fd1a85 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts @@ -1,16 +1,19 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; export class ApproveCommand { constructor( - private organizationService: OrganizationService, + private organizationService: DefaultOrganizationService, private organizationAuthRequestService: OrganizationAuthRequestService, + private accountService: AccountService, ) {} async run(organizationId: string, id: string): Promise { @@ -30,7 +33,17 @@ export class ApproveCommand { return Response.badRequest("`" + id + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations?.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -57,6 +70,7 @@ export class ApproveCommand { return new ApproveCommand( serviceContainer.organizationService, serviceContainer.organizationAuthRequestService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts index 767acea99f7..923545f15ef 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { MessageResponse } from "@bitwarden/cli/models/response/message.response"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -14,6 +16,7 @@ export class DenyAllCommand { constructor( private organizationService: OrganizationService, private organizationAuthRequestService: OrganizationAuthRequestService, + private accountService: AccountService, ) {} async run(organizationId: string): Promise { @@ -25,7 +28,17 @@ export class DenyAllCommand { return Response.badRequest("`" + organizationId + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -54,6 +67,7 @@ export class DenyAllCommand { return new DenyAllCommand( serviceContainer.organizationService, serviceContainer.organizationAuthRequestService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts index 87e633b2bee..ac5c285f8d7 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts @@ -1,8 +1,9 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -11,6 +12,7 @@ export class DenyCommand { constructor( private organizationService: OrganizationService, private organizationAuthRequestService: OrganizationAuthRequestService, + private accountServcie: AccountService, ) {} async run(organizationId: string, id: string): Promise { @@ -30,7 +32,17 @@ export class DenyCommand { return Response.badRequest("`" + id + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(this.accountServcie.activeAccount$.pipe(map((a) => a?.id))); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -57,6 +69,7 @@ export class DenyCommand { return new DenyCommand( serviceContainer.organizationService, serviceContainer.organizationAuthRequestService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts index 972be460df7..31a0e748175 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts @@ -1,9 +1,11 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { ListResponse } from "@bitwarden/cli/models/response/list.response"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -14,6 +16,7 @@ export class ListCommand { constructor( private organizationAuthRequestService: OrganizationAuthRequestService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} async run(organizationId: string): Promise { @@ -25,7 +28,17 @@ export class ListCommand { return Response.badRequest("`" + organizationId + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -46,6 +59,7 @@ export class ListCommand { return new ListCommand( serviceContainer.organizationAuthRequestService, serviceContainer.organizationService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/tsconfig.json b/bitwarden_license/bit-cli/tsconfig.json index 92a206f44db..4a972b540a7 100644 --- a/bitwarden_license/bit-cli/tsconfig.json +++ b/bitwarden_license/bit-cli/tsconfig.json @@ -18,13 +18,12 @@ "@bitwarden/auth/common": ["../../libs/auth/src/common"], "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], "@bitwarden/common/*": ["../../libs/common/src/*"], - "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/importer-core": ["../../libs/importer/src"], "@bitwarden/generator-core": ["../../libs/tools/generator/core/src"], "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"], "@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"], "@bitwarden/key-management": ["../../libs/key-management/src"], - "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], diff --git a/bitwarden_license/bit-common/jest.config.js b/bitwarden_license/bit-common/jest.config.js index a0441b01883..ab31a4c26ca 100644 --- a/bitwarden_license/bit-common/jest.config.js +++ b/bitwarden_license/bit-common/jest.config.js @@ -7,9 +7,17 @@ module.exports = { ...sharedConfig, displayName: "bit-common tests", testEnvironment: "jsdom", - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + { + "@bitwarden/common/spec": ["../../libs/common/spec"], + "@bitwarden/common": ["../../libs/common/src/*"], + "@bitwarden/admin-console/common": ["/libs/admin-console/src/common"], + ...(compilerOptions?.paths ?? {}), + }, + { + prefix: "/", + }, + ), setupFilesAfterEnv: ["/test.setup.ts"], transformIgnorePatterns: ["node_modules/(?!(.*\\.mjs$|@angular|rxjs|@bitwarden))"], moduleFileExtensions: ["ts", "js", "html", "mjs"], diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts index e893b2dfe8c..448399a8bb0 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts @@ -4,8 +4,8 @@ import { OrganizationUserApiService, OrganizationUserResetPasswordDetailsResponse, } from "@bitwarden/admin-console/common"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { KeyService } from "@bitwarden/key-management"; diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts index 4c4507e5cb8..025b021f83d 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts @@ -4,7 +4,7 @@ import { OrganizationUserApiService, OrganizationUserResetPasswordDetailsResponse, } from "@bitwarden/admin-console/common"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts index 947fc8a79d3..723d737d5bd 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts @@ -1,6 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { Opaque } from "type-fest"; + +import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeVariant } from "@bitwarden/components"; @@ -82,6 +85,7 @@ export type WeakPasswordScore = { * How many times a password has been exposed */ export type ExposedPasswordDetail = { + cipherId: string; exposedXTimes: number; } | null; @@ -113,3 +117,43 @@ export type AtRiskApplicationDetail = { applicationName: string; atRiskPasswordCount: number; }; + +export type AppAtRiskMembersDialogParams = { + members: MemberDetailsFlat[]; + applicationName: string; +}; + +/** + * Request to drop a password health report application + * Model is expected by the API endpoint + */ +export interface PasswordHealthReportApplicationDropRequest { + organizationId: OrganizationId; + passwordHealthReportApplicationIds: string[]; +} + +/** + * Response from the API after marking an app as critical + */ +export interface PasswordHealthReportApplicationsResponse { + id: PasswordHealthReportApplicationId; + organizationId: OrganizationId; + uri: string; +} +/* + * Request to save a password health report application + * Model is expected by the API endpoint + */ +export interface PasswordHealthReportApplicationsRequest { + organizationId: OrganizationId; + url: string; +} + +export enum DrawerType { + None = 0, + AppAtRiskMembers = 1, + OrgAtRiskMembers = 2, + OrgAtRiskApps = 3, +} + +export type PasswordHealthReportApplicationId = Opaque; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts index 838dc2c8241..5ed88c4cac9 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.spec.ts @@ -3,12 +3,14 @@ import { mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { CriticalAppsApiService } from "./critical-apps-api.service"; import { + PasswordHealthReportApplicationDropRequest, PasswordHealthReportApplicationId, PasswordHealthReportApplicationsRequest, PasswordHealthReportApplicationsResponse, -} from "./critical-apps.service"; +} from "../models/password-health"; + +import { CriticalAppsApiService } from "./critical-apps-api.service"; describe("CriticalAppsApiService", () => { let service: CriticalAppsApiService; @@ -76,4 +78,24 @@ describe("CriticalAppsApiService", () => { done(); }); }); + + it("should call apiService.send with correct parameters for DropCriticalApp", (done) => { + const request: PasswordHealthReportApplicationDropRequest = { + organizationId: "org1" as OrganizationId, + passwordHealthReportApplicationIds: ["123"], + }; + + apiService.send.mockReturnValue(Promise.resolve()); + + service.dropCriticalApp(request).subscribe(() => { + expect(apiService.send).toHaveBeenCalledWith( + "DELETE", + "/reports/password-health-report-application/", + request, + true, + true, + ); + done(); + }); + }); }); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts index edd2cf34b56..c02a3686dfd 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps-api.service.ts @@ -4,9 +4,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { + PasswordHealthReportApplicationDropRequest, PasswordHealthReportApplicationsRequest, PasswordHealthReportApplicationsResponse, -} from "./critical-apps.service"; +} from "../models/password-health"; export class CriticalAppsApiService { constructor(private apiService: ApiService) {} @@ -36,4 +37,16 @@ export class CriticalAppsApiService { return from(dbResponse as Promise); } + + dropCriticalApp(request: PasswordHealthReportApplicationDropRequest): Observable { + const dbResponse = this.apiService.send( + "DELETE", + "/reports/password-health-report-application/", + request, + true, + true, + ); + + return from(dbResponse as Promise); + } } diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts index c6c4562310e..64f55ccf99f 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts @@ -4,7 +4,7 @@ import { fakeAsync, flush } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; @@ -12,13 +12,14 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; -import { CriticalAppsApiService } from "./critical-apps-api.service"; import { - CriticalAppsService, PasswordHealthReportApplicationId, PasswordHealthReportApplicationsRequest, PasswordHealthReportApplicationsResponse, -} from "./critical-apps.service"; +} from "../models/password-health"; + +import { CriticalAppsApiService } from "./critical-apps-api.service"; +import { CriticalAppsService } from "./critical-apps.service"; describe("CriticalAppsService", () => { let service: CriticalAppsService; @@ -139,4 +140,54 @@ describe("CriticalAppsService", () => { expect(res).toHaveLength(2); }); }); + + it("should drop a critical app", async () => { + // arrange + const orgId = "org1" as OrganizationId; + const selectedUrl = "https://example.com"; + + const initialList = [ + { id: "id1", organizationId: "org1", uri: "https://example.com" }, + { id: "id2", organizationId: "org1", uri: "https://example.org" }, + ] as PasswordHealthReportApplicationsResponse[]; + + service.setAppsInListForOrg(initialList); + + // act + await service.dropCriticalApp(orgId, selectedUrl); + + // expectations + expect(criticalAppsApiService.dropCriticalApp).toHaveBeenCalledWith({ + organizationId: orgId, + passwordHealthReportApplicationIds: ["id1"], + }); + expect(service.getAppsListForOrg(orgId)).toBeTruthy(); + service.getAppsListForOrg(orgId).subscribe((res) => { + expect(res).toHaveLength(1); + expect(res[0].uri).toBe("https://example.org"); + }); + }); + + it("should not drop a critical app if it does not exist", async () => { + // arrange + const orgId = "org1" as OrganizationId; + const selectedUrl = "https://nonexistent.com"; + + const initialList = [ + { id: "id1", organizationId: "org1", uri: "https://example.com" }, + { id: "id2", organizationId: "org1", uri: "https://example.org" }, + ] as PasswordHealthReportApplicationsResponse[]; + + service.setAppsInListForOrg(initialList); + + // act + await service.dropCriticalApp(orgId, selectedUrl); + + // expectations + expect(criticalAppsApiService.dropCriticalApp).not.toHaveBeenCalled(); + expect(service.getAppsListForOrg(orgId)).toBeTruthy(); + service.getAppsListForOrg(orgId).subscribe((res) => { + expect(res).toHaveLength(2); + }); + }); }); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts index 10b7d3f1fbb..7db34757b96 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts @@ -12,14 +12,18 @@ import { takeUntil, zip, } from "rxjs"; -import { Opaque } from "type-fest"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { + PasswordHealthReportApplicationsRequest, + PasswordHealthReportApplicationsResponse, +} from "../models/password-health"; + import { CriticalAppsApiService } from "./critical-apps-api.service"; /* Retrieves and decrypts critical apps for a given organization @@ -94,6 +98,25 @@ export class CriticalAppsService { this.orgId.next(orgId); } + // Drop a critical app for a given organization + // Only one app may be dropped at a time + async dropCriticalApp(orgId: OrganizationId, selectedUrl: string) { + const app = this.criticalAppsList.value.find( + (f) => f.organizationId === orgId && f.uri === selectedUrl, + ); + + if (!app) { + return; + } + + await this.criticalAppsApiService.dropCriticalApp({ + organizationId: app.organizationId, + passwordHealthReportApplicationIds: [app.id], + }); + + this.criticalAppsList.next(this.criticalAppsList.value.filter((f) => f.uri !== selectedUrl)); + } + private retrieveCriticalApps( orgId: OrganizationId | null, ): Observable { @@ -144,16 +167,3 @@ export class CriticalAppsService { return await Promise.all(criticalAppsPromises); } } - -export interface PasswordHealthReportApplicationsRequest { - organizationId: OrganizationId; - url: string; -} - -export interface PasswordHealthReportApplicationsResponse { - id: PasswordHealthReportApplicationId; - organizationId: OrganizationId; - uri: string; -} - -export type PasswordHealthReportApplicationId = Opaque; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts index 42bab69fca4..668fb187251 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts @@ -1,10 +1,15 @@ import { BehaviorSubject } from "rxjs"; import { finalize } from "rxjs/operators"; -import { ApplicationHealthReportDetail } from "../models/password-health"; +import { + AppAtRiskMembersDialogParams, + ApplicationHealthReportDetail, + AtRiskApplicationDetail, + AtRiskMemberDetail, + DrawerType, +} from "../models/password-health"; import { RiskInsightsReportService } from "./risk-insights-report.service"; - export class RiskInsightsDataService { private applicationsSubject = new BehaviorSubject(null); @@ -22,6 +27,12 @@ export class RiskInsightsDataService { private dataLastUpdatedSubject = new BehaviorSubject(null); dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable(); + openDrawer = false; + activeDrawerType: DrawerType = DrawerType.None; + atRiskMemberDetails: AtRiskMemberDetail[] = []; + appAtRiskMembers: AppAtRiskMembersDialogParams | null = null; + atRiskAppDetails: AtRiskApplicationDetail[] | null = null; + constructor(private reportService: RiskInsightsReportService) {} /** @@ -57,4 +68,46 @@ export class RiskInsightsDataService { refreshApplicationsReport(organizationId: string): void { this.fetchApplicationsReport(organizationId, true); } + + isActiveDrawerType = (drawerType: DrawerType): boolean => { + return this.activeDrawerType === drawerType; + }; + + setDrawerForOrgAtRiskMembers = (atRiskMemberDetails: AtRiskMemberDetail[]): void => { + this.resetDrawer(DrawerType.OrgAtRiskMembers); + this.activeDrawerType = DrawerType.OrgAtRiskMembers; + this.atRiskMemberDetails = atRiskMemberDetails; + this.openDrawer = !this.openDrawer; + }; + + setDrawerForAppAtRiskMembers = ( + atRiskMembersDialogParams: AppAtRiskMembersDialogParams, + ): void => { + this.resetDrawer(DrawerType.None); + this.activeDrawerType = DrawerType.AppAtRiskMembers; + this.appAtRiskMembers = atRiskMembersDialogParams; + this.openDrawer = !this.openDrawer; + }; + + setDrawerForOrgAtRiskApps = (atRiskApps: AtRiskApplicationDetail[]): void => { + this.resetDrawer(DrawerType.OrgAtRiskApps); + this.activeDrawerType = DrawerType.OrgAtRiskApps; + this.atRiskAppDetails = atRiskApps; + this.openDrawer = !this.openDrawer; + }; + + closeDrawer = (): void => { + this.resetDrawer(DrawerType.None); + }; + + private resetDrawer = (drawerType: DrawerType): void => { + if (this.activeDrawerType !== drawerType) { + this.openDrawer = false; + } + + this.activeDrawerType = DrawerType.None; + this.atRiskMemberDetails = []; + this.appAtRiskMembers = null; + this.atRiskAppDetails = null; + }; } diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts index c3bcc59eca5..027760f678c 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts @@ -175,6 +175,7 @@ export class RiskInsightsReportService { ): Promise { const cipherHealthReports: CipherHealthReportDetail[] = []; const passwordUseMap = new Map(); + const exposedDetails = await this.findExposedPasswords(ciphers); for (const cipher of ciphers) { if (this.validateCipher(cipher)) { const weakPassword = this.findWeakPassword(cipher); @@ -189,7 +190,7 @@ export class RiskInsightsReportService { passwordUseMap.set(cipher.login.password, 1); } - const exposedPassword = await this.findExposedPassword(cipher); + const exposedPassword = exposedDetails.find((x) => x.cipherId === cipher.id); // Get the cipher members const cipherMembers = memberDetails.filter((x) => x.cipherId === cipher.id); @@ -255,13 +256,29 @@ export class RiskInsightsReportService { return appReports; } - private async findExposedPassword(cipher: CipherView): Promise { - const exposedCount = await this.auditService.passwordLeaked(cipher.login.password); - if (exposedCount > 0) { - const exposedDetail = { exposedXTimes: exposedCount } as ExposedPasswordDetail; - return exposedDetail; - } - return null; + private async findExposedPasswords(ciphers: CipherView[]): Promise { + const exposedDetails: ExposedPasswordDetail[] = []; + const promises: Promise[] = []; + + ciphers.forEach((ciph) => { + if (this.validateCipher(ciph)) { + const promise = this.auditService + .passwordLeaked(ciph.login.password) + .then((exposedCount) => { + if (exposedCount > 0) { + const detail = { + exposedXTimes: exposedCount, + cipherId: ciph.id, + } as ExposedPasswordDetail; + exposedDetails.push(detail); + } + }); + promises.push(promise); + } + }); + await Promise.all(promises); + + return exposedDetails; } private findWeakPassword(cipher: CipherView): WeakPasswordDetail { diff --git a/bitwarden_license/bit-common/tsconfig.json b/bitwarden_license/bit-common/tsconfig.json index ec1d3787f82..bc36576f1b3 100644 --- a/bitwarden_license/bit-common/tsconfig.json +++ b/bitwarden_license/bit-common/tsconfig.json @@ -18,7 +18,6 @@ "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"], "@bitwarden/key-management": ["../../libs/key-management/src"], - "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["../../libs/tools/card/src"], diff --git a/bitwarden_license/bit-web/jest.config.js b/bitwarden_license/bit-web/jest.config.js index 17b7139049a..9c9c61b2402 100644 --- a/bitwarden_license/bit-web/jest.config.js +++ b/bitwarden_license/bit-web/jest.config.js @@ -9,7 +9,15 @@ module.exports = { ...sharedConfig, preset: "jest-preset-angular", setupFilesAfterEnv: ["../../apps/web/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + { + "@bitwarden/common/spec": ["../../libs/common/spec"], + "@bitwarden/common": ["../../libs/common/src/*"], + "@bitwarden/admin-console/common": ["/libs/admin-console/src/common"], + ...(compilerOptions?.paths ?? {}), + }, + { + prefix: "/", + }, + ), }; diff --git a/bitwarden_license/bit-web/src/app/admin-console/.eslintrc.json b/bitwarden_license/bit-web/src/app/admin-console/.eslintrc.json deleted file mode 100644 index d55df3899e7..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../../../libs/admin-console/.eslintrc.json" -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts index ac8ad3112b9..744cf2c4674 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts @@ -10,8 +10,8 @@ import { OrganizationAuthRequestApiService } from "@bitwarden/bit-common/admin-c import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests/organization-auth-request.service"; import { PendingAuthRequestView } from "@bitwarden/bit-common/admin-console/auth-requests/pending-auth-request.view"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html index ac83491538e..c292d51ebda 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html @@ -4,7 +4,11 @@ -

+

{{ "claimedDomainsDesc" | i18n }} 0 ? response.data : []; this.dataSource.data = clients; this.manageOrganizations = (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; - const candidateOrgs = (await this.organizationService.getAll()).filter( - (o) => o.isOwner && o.providerId == null, - ); + const candidateOrgs = ( + await firstValueFrom(this.organizationService.organizations$(userId)) + ).filter((o) => o.isOwner && o.providerId == null); const allowedOrgsIds = await Promise.all( candidateOrgs.map((o) => this.organizationApiService.get(o.id)), ).then((orgs) => diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts index 6ab07cc0794..c5b949512d7 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -13,8 +13,8 @@ import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/ import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { DialogService } from "@bitwarden/components"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index 16f794bd6d2..03e47569a55 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -15,8 +15,8 @@ import { ProviderUserStatusType, ProviderUserType } from "@bitwarden/common/admi import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index a20dd1379e2..bf82fbd160b 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -37,25 +37,5 @@ > - - {{ "providerClientVaultPrivacyNotification" | i18n }} - - {{ "contactBitwardenSupport" | i18n }} . - diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts index 3f1a7ff3989..7e47da95e2b 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts @@ -10,27 +10,15 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { BannerModule, IconModule, LinkModule } from "@bitwarden/components"; +import { IconModule } from "@bitwarden/components"; import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo"; import { WebLayoutModule } from "@bitwarden/web-vault/app/layouts/web-layout.module"; -import { ProviderClientVaultPrivacyBannerService } from "./services/provider-client-vault-privacy-banner.service"; - @Component({ selector: "providers-layout", templateUrl: "providers-layout.component.html", standalone: true, - imports: [ - CommonModule, - RouterModule, - JslibModule, - WebLayoutModule, - IconModule, - LinkModule, - BannerModule, - ], + imports: [CommonModule, RouterModule, JslibModule, WebLayoutModule, IconModule], }) export class ProvidersLayoutComponent implements OnInit, OnDestroy { protected readonly logo = ProviderPortalLogo; @@ -41,15 +29,9 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy { protected isBillable: Observable; protected canAccessBilling$: Observable; - protected showProviderClientVaultPrivacyWarningBanner$ = this.configService.getFeatureFlag$( - FeatureFlag.ProviderClientVaultPrivacyBanner, - ); - constructor( private route: ActivatedRoute, private providerService: ProviderService, - private configService: ConfigService, - protected providerClientVaultPrivacyBannerService: ProviderClientVaultPrivacyBannerService, ) {} ngOnInit() { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 37cb9618b60..3310be7ba36 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -17,6 +17,7 @@ import { ProviderSubscriptionComponent, ProviderSubscriptionStatusComponent, } from "../../billing/providers"; +import { AddExistingOrganizationDialogComponent } from "../../billing/providers/clients/add-existing-organization-dialog.component"; import { AddOrganizationComponent } from "./clients/add-organization.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; @@ -63,6 +64,7 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr SetupProviderComponent, UserAddEditComponent, AddEditMemberDialogComponent, + AddExistingOrganizationDialogComponent, CreateClientDialogComponent, ManageClientNameDialogComponent, ManageClientSubscriptionDialogComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/provider-client-vault-privacy-banner.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/provider-client-vault-privacy-banner.service.ts deleted file mode 100644 index c347f5c2aae..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/provider-client-vault-privacy-banner.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { - StateProvider, - AC_BANNERS_DISMISSED_DISK, - UserKeyDefinition, -} from "@bitwarden/common/platform/state"; - -export const SHOW_BANNER_KEY = new UserKeyDefinition( - AC_BANNERS_DISMISSED_DISK, - "showProviderClientVaultPrivacyBanner", - { - deserializer: (b) => b, - clearOn: [], - }, -); - -/** Displays a banner warning provider users that client organization vaults - * will soon become inaccessible directly. */ -@Injectable({ providedIn: "root" }) -export class ProviderClientVaultPrivacyBannerService { - private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY); - - showBanner$ = this._showBanner.state$; - - constructor(private stateProvider: StateProvider) {} - - async hideBanner() { - await this._showBanner.update(() => false); - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts index 264b43aee9d..d3482ea67a5 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts @@ -1,15 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; +import { switchMap } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { ProviderAddOrganizationRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-add-organization.request"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { PlanType } from "@bitwarden/common/billing/enums"; import { CreateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/create-client-organization.request"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { KeyService } from "@bitwarden/key-management"; @@ -23,6 +28,8 @@ export class WebProviderService { private i18nService: I18nService, private encryptService: EncryptService, private billingApiService: BillingApiServiceAbstraction, + private stateProvider: StateProvider, + private providerApiService: ProviderApiServiceAbstraction, ) {} async addOrganizationToProvider(providerId: string, organizationId: string) { @@ -40,6 +47,22 @@ export class WebProviderService { return response; } + async addOrganizationToProviderVNext(providerId: string, organizationId: string): Promise { + const orgKey = await firstValueFrom( + this.stateProvider.activeUserId$.pipe( + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]), + ), + ); + const providerKey = await this.keyService.getProviderKey(providerId); + const encryptedOrgKey = await this.encryptService.encrypt(orgKey.key, providerKey); + await this.providerApiService.addOrganizationToProvider(providerId, { + key: encryptedOrgKey.encryptedString, + organizationId, + }); + await this.syncService.fullSync(true); + } + async createClientOrganization( providerId: string, name: string, diff --git a/bitwarden_license/bit-web/src/app/app.component.ts b/bitwarden_license/bit-web/src/app/app.component.ts index 1e0f60e2cd2..dd814f5c0d2 100644 --- a/bitwarden_license/bit-web/src/app/app.component.ts +++ b/bitwarden_license/bit-web/src/app/app.component.ts @@ -20,17 +20,10 @@ export class AppComponent extends BaseAppComponent implements OnInit { this.policyListService.addPolicies([ new MaximumVaultTimeoutPolicy(), new DisablePersonalVaultExportPolicy(), + new FreeFamiliesSponsorshipPolicy(), + new ActivateAutofillPolicy(), ]); - this.configService - .getFeatureFlag(FeatureFlag.DisableFreeFamiliesSponsorship) - .then((isFreeFamilyEnabled) => { - if (isFreeFamilyEnabled) { - this.policyListService.addPolicies([new FreeFamiliesSponsorshipPolicy()]); - } - this.policyListService.addPolicies([new ActivateAutofillPolicy()]); - }); - this.configService.getFeatureFlag(FeatureFlag.IdpAutoSubmitLogin).then((enabled) => { if ( enabled && diff --git a/bitwarden_license/bit-web/src/app/app.module.ts b/bitwarden_license/bit-web/src/app/app.module.ts index 3a78ae0ed01..833a67e1515 100644 --- a/bitwarden_license/bit-web/src/app/app.module.ts +++ b/bitwarden_license/bit-web/src/app/app.module.ts @@ -4,7 +4,6 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { RouterModule } from "@angular/router"; -import { InfiniteScrollDirective } from "ngx-infinite-scroll"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CoreModule } from "@bitwarden/web-vault/app/core"; @@ -37,7 +36,6 @@ import { AccessIntelligenceModule } from "./tools/access-intelligence/access-int FormsModule, ReactiveFormsModule, CoreModule, - InfiniteScrollDirective, DragDropModule, AppRoutingModule, OssRoutingModule, diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 6449ef7a701..779cb96146c 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -9,13 +9,17 @@ import { Validators, } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, Observable, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MemberDecryptionType, OpenIdConnectRedirectBehavior, @@ -28,6 +32,7 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api"; import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -195,6 +200,7 @@ export class SsoComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private organizationService: OrganizationService, + private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private configService: ConfigService, private toastService: ToastService, @@ -260,7 +266,12 @@ export class SsoComponent implements OnInit, OnDestroy { } async load() { - this.organization = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const ssoSettings = await this.organizationApiService.getSso(this.organizationId); this.populateForm(ssoSettings); diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.html new file mode 100644 index 00000000000..a22484ed92d --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.html @@ -0,0 +1,73 @@ + + + {{ "addExistingOrganization" | i18n }} + + + +

{{ "selectOrganizationProviderPortal" | i18n }}

+ + + + {{ "name" | i18n }} + {{ "assigned" | i18n }} + + + + + + + + + {{ addable.name }} +
+ {{ "assignedExceedsAvailable" | i18n }} +
+ + {{ addable.seats }} + + + + +
+
+

+ {{ "noOrganizations" | i18n }} +

+
+ +

{{ "yourProviderSubscriptionCredit" | i18n }}

+

{{ "doYouWantToAddThisOrg" | i18n: dialogParams.provider.name }}

+
+
{{ "organization" | i18n }}: {{ selectedOrganization.name }}
+
{{ "billingPlan" | i18n }}: {{ selectedOrganization.plan }}
+
{{ "assignedSeats" | i18n }}: {{ selectedOrganization.seats }}
+
+
+
+ + + + + diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts new file mode 100644 index 00000000000..3df0693d091 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts @@ -0,0 +1,82 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, OnInit } from "@angular/core"; + +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; + +export type AddExistingOrganizationDialogParams = { + provider: Provider; +}; + +export enum AddExistingOrganizationDialogResultType { + Closed = "closed", + Submitted = "submitted", +} + +@Component({ + templateUrl: "./add-existing-organization-dialog.component.html", +}) +export class AddExistingOrganizationDialogComponent implements OnInit { + protected loading: boolean = true; + + addableOrganizations: AddableOrganizationResponse[] = []; + selectedOrganization?: AddableOrganizationResponse; + + protected readonly ResultType = AddExistingOrganizationDialogResultType; + + constructor( + @Inject(DIALOG_DATA) protected dialogParams: AddExistingOrganizationDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, + private providerApiService: ProviderApiServiceAbstraction, + private toastService: ToastService, + private webProviderService: WebProviderService, + ) {} + + async ngOnInit() { + this.addableOrganizations = await this.providerApiService.getProviderAddableOrganizations( + this.dialogParams.provider.id, + ); + this.loading = false; + } + + addExistingOrganization = async (): Promise => { + if (this.selectedOrganization) { + await this.webProviderService.addOrganizationToProviderVNext( + this.dialogParams.provider.id, + this.selectedOrganization.id, + ); + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("addedExistingOrganization"), + }); + + this.dialogRef.close(this.ResultType.Submitted); + } + }; + + selectOrganization(organizationId: string) { + this.selectedOrganization = this.addableOrganizations.find( + (organization) => organization.id === organizationId, + ); + } + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig< + AddExistingOrganizationDialogParams, + DialogRef + >, + ) => + dialogService.open< + AddExistingOrganizationDialogResultType, + AddExistingOrganizationDialogParams + >(AddExistingOrganizationDialogComponent, dialogConfig); +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html index 7c560e49579..077aeb6c124 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html @@ -1,9 +1,39 @@ - - - {{ "addNewOrganization" | i18n }} - + + + + + + + + + + + {{ "addNewOrganization" | i18n }} + + diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts index ee2c541e72f..07434369122 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts @@ -11,6 +11,8 @@ import { Provider } from "@bitwarden/common/admin-console/models/domain/provider import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { @@ -25,6 +27,10 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; +import { + AddExistingOrganizationDialogComponent, + AddExistingOrganizationDialogResultType, +} from "./add-existing-organization-dialog.component"; import { CreateClientDialogResultType, openCreateClientDialog, @@ -62,6 +68,9 @@ export class ManageClientsComponent { protected searchControl = new FormControl("", { nonNullable: true }); protected plans: PlanResponse[] = []; + protected addExistingOrgsFromProviderPortal$ = this.configService.getFeatureFlag$( + FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal, + ); constructor( private billingApiService: BillingApiServiceAbstraction, @@ -73,6 +82,7 @@ export class ManageClientsComponent { private toastService: ToastService, private validationService: ValidationService, private webProviderService: WebProviderService, + private configService: ConfigService, ) { this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { this.searchControl.setValue(queryParams.search); @@ -111,19 +121,30 @@ export class ManageClientsComponent { async load() { this.provider = await firstValueFrom(this.providerService.get$(this.providerId)); - this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin; - - const clients = (await this.billingApiService.getProviderClientOrganizations(this.providerId)) - .data; - - this.dataSource.data = clients; - + this.dataSource.data = ( + await this.billingApiService.getProviderClientOrganizations(this.providerId) + ).data; this.plans = (await this.billingApiService.getPlans()).data; - this.loading = false; } + addExistingOrganization = async () => { + if (this.provider) { + const reference = AddExistingOrganizationDialogComponent.open(this.dialogService, { + data: { + provider: this.provider, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === AddExistingOrganizationDialogResultType.Submitted) { + await this.load(); + } + } + }; + createClient = async () => { const reference = openCreateClientDialog(this.dialogService, { data: { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts index a1f7564156c..d042c4d904e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts @@ -1,7 +1,13 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router"; +import { firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; /** @@ -10,13 +16,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv export const organizationEnabledGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => { const syncService = inject(SyncService); const orgService = inject(OrganizationService); + const accountService = inject(AccountService); /** Workaround to avoid service initialization race condition. */ if ((await syncService.getLastSync()) == null) { await syncService.fullSync(false); } - const org = await orgService.get(route.params.organizationId); + const userId = await firstValueFrom(getUserId(accountService.activeAccount$)); + const org = await firstValueFrom( + orgService.organizations$(userId).pipe(getOrganizationById(route.params.organizationId)), + ); if (org == null || !org.canAccessSecretsManager) { return createUrlTreeFromSnapshot(route, ["/"]); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts index 2a36bf1cbbc..39576bc8dc6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts @@ -5,8 +5,11 @@ import { createUrlTreeFromSnapshot, RouterStateSnapshot, } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; /** @@ -18,13 +21,15 @@ export const canActivateSM: CanActivateFn = async ( ) => { const syncService = inject(SyncService); const orgService = inject(OrganizationService); + const accountService = inject(AccountService); /** Workaround to avoid service initialization race condition. */ if ((await syncService.getLastSync()) == null) { await syncService.fullSync(false); } - const orgs = await orgService.getAll(); + const userId = await firstValueFrom(getUserId(accountService.activeAccount$)); + const orgs = await firstValueFrom(orgService.organizations$(userId)); const smOrg = orgs.find((o) => o.canAccessSecretsManager); if (smOrg) { return createUrlTreeFromSnapshot(route, ["/sm", smOrg.id]); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts index adf01afd10c..6594b71a14c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts @@ -15,8 +15,13 @@ import { takeUntil, } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SecretsManagerLogo } from "@bitwarden/web-vault/app/layouts/secrets-manager-logo"; import { OrganizationCounts } from "../models/view/counts.view"; @@ -41,6 +46,7 @@ export class NavigationComponent implements OnInit, OnDestroy { constructor( protected route: ActivatedRoute, private organizationService: OrganizationService, + private accountService: AccountService, private countService: CountService, private projectService: ProjectService, private secretService: SecretService, @@ -50,7 +56,15 @@ export class NavigationComponent implements OnInit, OnDestroy { ngOnInit() { const org$ = this.route.params.pipe( - concatMap((params) => this.organizationService.get(params.organizationId)), + concatMap((params) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ), + ), + ), distinctUntilChanged(), takeUntil(this.destroy$), ); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index a95192e0d91..7eb28b2bc2d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -20,15 +20,20 @@ import { import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; 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 { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; import { TrialFlowService } from "@bitwarden/web-vault/app/billing/services/trial-flow.service"; -import { FreeTrial } from "@bitwarden/web-vault/app/core/types/free-trial"; +import { FreeTrial } from "@bitwarden/web-vault/app/billing/types/free-trial"; import { OrganizationCounts } from "../models/view/counts.view"; import { ProjectListView } from "../models/view/project-list.view"; @@ -112,6 +117,7 @@ export class OverviewComponent implements OnInit, OnDestroy { private serviceAccountService: ServiceAccountService, private dialogService: DialogService, private organizationService: OrganizationService, + private accountService: AccountService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private smOnboardingTasksService: SMOnboardingTasksService, @@ -130,7 +136,15 @@ export class OverviewComponent implements OnInit, OnDestroy { distinctUntilChanged(), ); - const org$ = orgId$.pipe(switchMap((orgId) => this.organizationService.get(orgId))); + const org$ = orgId$.pipe( + switchMap((orgId) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService.organizations$(userId).pipe(getOrganizationById(orgId)), + ), + ), + ), + ); org$.pipe(takeUntil(this.destroy$)).subscribe((org) => { this.organizationId = org.id; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts index 78d367776d4..159b90d5432 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts @@ -3,10 +3,15 @@ import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { RouterService } from "@bitwarden/web-vault/app/core"; @@ -32,6 +37,8 @@ describe("Project Redirect Guard", () => { let i18nServiceMock: MockProxy; let toastService: MockProxy; let router: Router; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; const smOrg1 = { id: "123", canAccessSecretsManager: true } as Organization; const projectView = { @@ -50,6 +57,7 @@ describe("Project Redirect Guard", () => { projectServiceMock = mock(); i18nServiceMock = mock(); toastService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ imports: [ @@ -71,6 +79,7 @@ describe("Project Redirect Guard", () => { ], providers: [ { provide: OrganizationService, useValue: organizationService }, + { provide: AccountService, useValue: accountService }, { provide: RouterService, useValue: routerService }, { provide: ProjectService, useValue: projectServiceMock }, { provide: I18nService, useValue: i18nServiceMock }, @@ -83,7 +92,7 @@ describe("Project Redirect Guard", () => { it("redirects to sm/{orgId}/projects/{projectId} if project exists", async () => { // Arrange - organizationService.getAll.mockResolvedValue([smOrg1]); + organizationService.organizations$.mockReturnValue(of([smOrg1])); projectServiceMock.getByProjectId.mockReturnValue(Promise.resolve(projectView)); // Act @@ -95,7 +104,7 @@ describe("Project Redirect Guard", () => { it("redirects to sm/projects if project does not exist", async () => { // Arrange - organizationService.getAll.mockResolvedValue([smOrg1]); + organizationService.organizations$.mockReturnValue(of([smOrg1])); // Act await router.navigateByUrl("sm/123/projects/124"); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts index ee2395b3f83..8c9f894f8f6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts @@ -4,8 +4,8 @@ import { Injectable } from "@angular/core"; import { Subject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { KeyService } from "@bitwarden/key-management"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html index 1ab8b7e0196..d9919ef6bac 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html @@ -2,7 +2,7 @@
- - diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts deleted file mode 100644 index d6a757fe897..00000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { DIALOG_DATA } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, Inject } from "@angular/core"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { MemberDetailsFlat } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; -import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; - -type AppAtRiskMembersDialogParams = { - members: MemberDetailsFlat[]; - applicationName: string; -}; - -export const openAppAtRiskMembersDialog = ( - dialogService: DialogService, - dialogConfig: AppAtRiskMembersDialogParams, -) => - dialogService.open(AppAtRiskMembersDialogComponent, { - data: dialogConfig, - }); - -@Component({ - standalone: true, - templateUrl: "./app-at-risk-members-dialog.component.html", - imports: [ButtonModule, CommonModule, JslibModule, DialogModule], -}) -export class AppAtRiskMembersDialogComponent { - protected members: MemberDetailsFlat[]; - protected applicationName: string; - - constructor(@Inject(DIALOG_DATA) private params: AppAtRiskMembersDialogParams) { - this.members = params.members; - this.applicationName = params.applicationName; - } -} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html index 87b21c7c755..72e60c470b0 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html @@ -28,7 +28,7 @@

{{ "criticalApplications" | i18n }}

- @@ -43,7 +43,7 @@ > {{ r.memberCount }} + + + + + + + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts index 450f0d5d660..4d820a3cc66 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts @@ -15,21 +15,22 @@ import { ApplicationHealthReportDetailWithCriticalFlag, ApplicationHealthReportSummary, } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { - DialogService, Icons, NoItemsModule, SearchModule, TableDataSource, + ToastService, } from "@bitwarden/components"; import { CardComponent } from "@bitwarden/tools-card"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; -import { openAppAtRiskMembersDialog } from "./app-at-risk-members-dialog.component"; -import { OrgAtRiskAppsDialogComponent } from "./org-at-risk-apps-dialog.component"; -import { OrgAtRiskMembersDialogComponent } from "./org-at-risk-members-dialog.component"; import { RiskInsightsTabType } from "./risk-insights.component"; @Component({ @@ -37,6 +38,7 @@ import { RiskInsightsTabType } from "./risk-insights.component"; selector: "tools-critical-applications", templateUrl: "./critical-applications.component.html", imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule], + providers: [], }) export class CriticalApplicationsComponent implements OnInit { protected dataSource = new TableDataSource(); @@ -47,8 +49,12 @@ export class CriticalApplicationsComponent implements OnInit { protected organizationId: string; protected applicationSummary = {} as ApplicationHealthReportSummary; noItemsIcon = Icons.Security; + isNotificationsFeatureEnabled: boolean = false; - ngOnInit() { + async ngOnInit() { + this.isNotificationsFeatureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.EnableRiskInsightsNotifications, + ); this.organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; combineLatest([ this.dataService.applications$, @@ -80,13 +86,38 @@ export class CriticalApplicationsComponent implements OnInit { }); }; + unmarkAsCriticalApp = async (hostname: string) => { + try { + await this.criticalAppsService.dropCriticalApp( + this.organizationId as OrganizationId, + hostname, + ); + } catch { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + return; + } + + this.toastService.showToast({ + message: this.i18nService.t("criticalApplicationSuccessfullyUnmarked"), + variant: "success", + title: this.i18nService.t("success"), + }); + this.dataSource.data = this.dataSource.data.filter((app) => app.applicationName !== hostname); + }; + constructor( protected activatedRoute: ActivatedRoute, protected router: Router, + protected toastService: ToastService, protected dataService: RiskInsightsDataService, protected criticalAppsService: CriticalAppsService, protected reportService: RiskInsightsReportService, - protected dialogService: DialogService, + protected i18nService: I18nService, + private configService: ConfigService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) @@ -94,24 +125,23 @@ export class CriticalApplicationsComponent implements OnInit { } showAppAtRiskMembers = async (applicationName: string) => { - openAppAtRiskMembersDialog(this.dialogService, { + const data = { members: this.dataSource.data.find((app) => app.applicationName === applicationName) ?.atRiskMemberDetails ?? [], applicationName, - }); + }; + this.dataService.setDrawerForAppAtRiskMembers(data); }; showOrgAtRiskMembers = async () => { - this.dialogService.open(OrgAtRiskMembersDialogComponent, { - data: this.reportService.generateAtRiskMemberList(this.dataSource.data), - }); + const data = this.reportService.generateAtRiskMemberList(this.dataSource.data); + this.dataService.setDrawerForOrgAtRiskMembers(data); }; showOrgAtRiskApps = async () => { - this.dialogService.open(OrgAtRiskAppsDialogComponent, { - data: this.reportService.generateAtRiskApplicationList(this.dataSource.data), - }); + const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data); + this.dataService.setDrawerForOrgAtRiskApps(data); }; trackByFunction(_: number, item: ApplicationHealthReportDetailWithCriticalFlag) { diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html deleted file mode 100644 index 298011b2157..00000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html +++ /dev/null @@ -1,25 +0,0 @@ - - - {{ "atRiskApplicationsWithCount" | i18n: atRiskApps.length }} - - -
- {{ "atRiskApplicationsDescription" | i18n }} -
-
{{ "application" | i18n }}
-
{{ "atRiskPasswords" | i18n }}
-
- -
-
{{ app.applicationName }}
-
{{ app.atRiskPasswordCount }}
-
-
-
-
- - - -
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts deleted file mode 100644 index 0ae00f60874..00000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DIALOG_DATA } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, Inject } from "@angular/core"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { AtRiskApplicationDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; -import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; - -export const openOrgAtRiskMembersDialog = ( - dialogService: DialogService, - dialogConfig: AtRiskApplicationDetail[], -) => - dialogService.open(OrgAtRiskAppsDialogComponent, { - data: dialogConfig, - }); - -@Component({ - standalone: true, - templateUrl: "./org-at-risk-apps-dialog.component.html", - imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule], -}) -export class OrgAtRiskAppsDialogComponent { - constructor(@Inject(DIALOG_DATA) protected atRiskApps: AtRiskApplicationDetail[]) {} -} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html deleted file mode 100644 index 1f1de103661..00000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html +++ /dev/null @@ -1,25 +0,0 @@ - - - {{ "atRiskMembersWithCount" | i18n: atRiskMembers.length }} - - -
- {{ "atRiskMembersDescription" | i18n }} -
-
{{ "email" | i18n }}
-
{{ "atRiskPasswords" | i18n }}
-
- -
-
{{ member.email }}
-
{{ member.atRiskPasswordCount }}
-
-
-
-
- - - -
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts deleted file mode 100644 index 72518843d94..00000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DIALOG_DATA } from "@angular/cdk/dialog"; -import { CommonModule } from "@angular/common"; -import { Component, Inject } from "@angular/core"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { AtRiskMemberDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; -import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; - -export const openOrgAtRiskMembersDialog = ( - dialogService: DialogService, - dialogConfig: AtRiskMemberDetail[], -) => - dialogService.open(OrgAtRiskMembersDialogComponent, { - data: dialogConfig, - }); - -@Component({ - standalone: true, - templateUrl: "./org-at-risk-members-dialog.component.html", - imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule], -}) -export class OrgAtRiskMembersDialogComponent { - constructor(@Inject(DIALOG_DATA) protected atRiskMembers: AtRiskMemberDetail[]) {} -} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts index 1e9e4171bc3..852e9adafb3 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts @@ -10,8 +10,12 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TableModule } from "@bitwarden/components"; import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; @@ -24,6 +28,7 @@ describe("PasswordHealthMembersUriComponent", () => { let fixture: ComponentFixture; let cipherServiceMock: MockProxy; const passwordHealthServiceMock = mock(); + const userId = Utils.newGuid() as UserId; const activeRouteParams = convertToParamMap({ organizationId: "orgId" }); @@ -36,6 +41,7 @@ describe("PasswordHealthMembersUriComponent", () => { { provide: I18nService, useValue: mock() }, { provide: AuditService, useValue: mock() }, { provide: OrganizationService, useValue: mock() }, + { provide: AccountService, useValue: mockAccountServiceWith(userId) }, { provide: PasswordStrengthServiceAbstraction, useValue: mock(), diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index ae8bd94e5f3..a368f5c0c18 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -1,57 +1,126 @@ -
{{ "accessIntelligence" | i18n }}
-

{{ "riskInsights" | i18n }}

-
- {{ "reviewAtRiskPasswords" | i18n }} -
-
- - {{ - "dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a") - }} - - - {{ "refresh" | i18n }} - - - + +
{{ "accessIntelligence" | i18n }}
+

{{ "riskInsights" | i18n }}

+
+ {{ "reviewAtRiskPasswords" | i18n }} +
+
+ + {{ + "dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a") + }} + + + {{ "refresh" | i18n }} + + + + - -
- - - - - - - - {{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }} - - - - - - - - - - - - - +
+ + + + + + + + {{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }} + + + + + + + + + + + + + + + + + + + + {{ + "atRiskMembersDescription" | i18n + }} +
+
{{ "email" | i18n }}
+
{{ "atRiskPasswords" | i18n }}
+
+ +
+
{{ member.email }}
+
{{ member.atRiskPasswordCount }}
+
+
+
+
+ + + + + +
+ {{ "atRiskMembersWithCount" | i18n: dataService.appAtRiskMembers.members.length }} +
+
+ {{ + "atRiskMembersDescriptionWithApp" | i18n: dataService.appAtRiskMembers.applicationName + }} +
+
+ +
{{ member.email }}
+
+
+
+
+ + + + + + + {{ + "atRiskApplicationsDescription" | i18n + }} +
+
{{ "application" | i18n }}
+
{{ "atRiskPasswords" | i18n }}
+
+ +
+
{{ app.applicationName }}
+
{{ app.atRiskPasswordCount }}
+
+
+
+
+
+
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts index 5adb0d32945..20dc320de20 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts @@ -2,22 +2,33 @@ import { CommonModule } from "@angular/common"; import { Component, DestroyRef, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; -import { Observable, EMPTY } from "rxjs"; +import { EMPTY, Observable } from "rxjs"; import { map, switchMap } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { - RiskInsightsDataService, CriticalAppsService, - PasswordHealthReportApplicationsResponse, + RiskInsightsDataService, } from "@bitwarden/bit-common/tools/reports/risk-insights"; -import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { + ApplicationHealthReportDetail, + DrawerType, + PasswordHealthReportApplicationsResponse, +} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; // eslint-disable-next-line no-restricted-imports -- used for dependency injection import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; +import { + AsyncActionsModule, + ButtonModule, + DrawerBodyComponent, + DrawerComponent, + DrawerHeaderComponent, + LayoutComponent, + TabsModule, +} from "@bitwarden/components"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { AllApplicationsComponent } from "./all-applications.component"; @@ -49,6 +60,10 @@ export enum RiskInsightsTabType { PasswordHealthMembersURIComponent, NotifiedMembersTableComponent, TabsModule, + DrawerComponent, + DrawerBodyComponent, + DrawerHeaderComponent, + LayoutComponent, ], }) export class RiskInsightsComponent implements OnInit { @@ -75,7 +90,7 @@ export class RiskInsightsComponent implements OnInit { private route: ActivatedRoute, private router: Router, private configService: ConfigService, - private dataService: RiskInsightsDataService, + protected dataService: RiskInsightsDataService, private criticalAppsService: CriticalAppsService, ) { this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { @@ -135,5 +150,13 @@ export class RiskInsightsComponent implements OnInit { queryParams: { tabIndex: newIndex }, queryParamsHandling: "merge", }); + + // close drawer when tabs are changed + this.dataService.closeDrawer(); + } + + // Get a list of drawer types + get drawerTypes(): typeof DrawerType { + return DrawerType; } } diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts index 321aae165c5..52ec2901031 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts @@ -93,17 +93,16 @@ export class MemberAccessReportComponent implements OnInit { }); }; - edit = async (user: MemberAccessReportView | null): Promise => { + edit = async (user: MemberAccessReportView): Promise => { const dialog = openUserAddEditDialog(this.dialogService, { data: { + kind: "Edit", name: this.userNamePipe.transform(user), organizationId: this.organizationId, - organizationUserId: user != null ? user.userGuid : null, - allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [], - usesKeyConnector: user?.usesKeyConnector, + organizationUserId: user.userGuid, + usesKeyConnector: user.usesKeyConnector, isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: MemberDialogTab.Role, - numSeatsUsed: this.dataSource.data.length, }, }); diff --git a/bitwarden_license/bit-web/src/app/vault/services/abstractions/admin-task.abstraction.ts b/bitwarden_license/bit-web/src/app/vault/services/abstractions/admin-task.abstraction.ts new file mode 100644 index 00000000000..014c9daa783 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/vault/services/abstractions/admin-task.abstraction.ts @@ -0,0 +1,34 @@ +import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; +import { SecurityTask, SecurityTaskStatus, SecurityTaskType } from "@bitwarden/vault"; + +/** + * Request type for creating tasks. + * @property cipherId - Optional. The ID of the cipher to create the task for. + * @property type - The type of task to create. Currently defined as "updateAtRiskCredential". + */ +export type CreateTasksRequest = Readonly<{ + cipherId?: CipherId; + type: SecurityTaskType.UpdateAtRiskCredential; +}>; + +export abstract class AdminTaskService { + /** + * Retrieves all tasks for a given organization. + * @param organizationId - The ID of the organization to retrieve tasks for. + * @param status - Optional. The status of the tasks to retrieve. + */ + abstract getAllTasks( + organizationId: OrganizationId, + status?: SecurityTaskStatus | undefined, + ): Promise; + + /** + * Creates multiple tasks for a given organization and sends out notifications to applicable users. + * @param organizationId - The ID of the organization to create tasks for. + * @param tasks - The tasks to create. + */ + abstract bulkCreateTasks( + organizationId: OrganizationId, + tasks: CreateTasksRequest[], + ): Promise; +} diff --git a/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.spec.ts b/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.spec.ts new file mode 100644 index 00000000000..d6a686a071a --- /dev/null +++ b/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.spec.ts @@ -0,0 +1,65 @@ +import { MockProxy, mock } from "jest-mock-extended"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; +import { SecurityTaskStatus, SecurityTaskType } from "@bitwarden/vault"; + +import { CreateTasksRequest } from "./abstractions/admin-task.abstraction"; +import { DefaultAdminTaskService } from "./default-admin-task.service"; + +describe("DefaultAdminTaskService", () => { + let defaultAdminTaskService: DefaultAdminTaskService; + let apiService: MockProxy; + + beforeEach(() => { + apiService = mock(); + defaultAdminTaskService = new DefaultAdminTaskService(apiService); + }); + + describe("getAllTasks", () => { + it("should call the api service with the correct parameters with status", async () => { + const organizationId = "orgId" as OrganizationId; + const status = SecurityTaskStatus.Pending; + const expectedUrl = `/tasks/organization?organizationId=${organizationId}&status=0`; + + await defaultAdminTaskService.getAllTasks(organizationId, status); + + expect(apiService.send).toHaveBeenCalledWith("GET", expectedUrl, null, true, true); + }); + + it("should call the api service with the correct parameters without status", async () => { + const organizationId = "orgId" as OrganizationId; + const expectedUrl = `/tasks/organization?organizationId=${organizationId}`; + + await defaultAdminTaskService.getAllTasks(organizationId); + + expect(apiService.send).toHaveBeenCalledWith("GET", expectedUrl, null, true, true); + }); + }); + + describe("bulkCreateTasks", () => { + it("should call the api service with the correct parameters", async () => { + const organizationId = "orgId" as OrganizationId; + const tasks: CreateTasksRequest[] = [ + { + cipherId: "cipherId-1" as CipherId, + type: SecurityTaskType.UpdateAtRiskCredential, + }, + { + cipherId: "cipherId-2" as CipherId, + type: SecurityTaskType.UpdateAtRiskCredential, + }, + ]; + + await defaultAdminTaskService.bulkCreateTasks(organizationId, tasks); + + expect(apiService.send).toHaveBeenCalledWith( + "POST", + `/tasks/${organizationId}/bulk-create`, + tasks, + true, + true, + ); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.ts b/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.ts new file mode 100644 index 00000000000..442fde9dbf6 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + SecurityTask, + SecurityTaskData, + SecurityTaskResponse, + SecurityTaskStatus, +} from "@bitwarden/vault"; + +import { AdminTaskService, CreateTasksRequest } from "./abstractions/admin-task.abstraction"; + +@Injectable() +export class DefaultAdminTaskService implements AdminTaskService { + constructor(private apiService: ApiService) {} + + async getAllTasks( + organizationId: OrganizationId, + status?: SecurityTaskStatus | undefined, + ): Promise { + const queryParams = new URLSearchParams(); + + queryParams.append("organizationId", organizationId); + if (status !== undefined) { + queryParams.append("status", status.toString()); + } + + const r = await this.apiService.send( + "GET", + `/tasks/organization?${queryParams.toString()}`, + null, + true, + true, + ); + const response = new ListResponse(r, SecurityTaskResponse); + + return response.data.map((d) => new SecurityTask(new SecurityTaskData(d))); + } + + async bulkCreateTasks( + organizationId: OrganizationId, + tasks: CreateTasksRequest[], + ): Promise { + await this.apiService.send("POST", `/tasks/${organizationId}/bulk-create`, tasks, true, true); + } +} diff --git a/bitwarden_license/bit-web/tsconfig.build.json b/bitwarden_license/bit-web/tsconfig.build.json index 9bebbeb5061..6313ce27863 100644 --- a/bitwarden_license/bit-web/tsconfig.build.json +++ b/bitwarden_license/bit-web/tsconfig.build.json @@ -9,6 +9,6 @@ ], "include": [ "../../apps/web/src/connectors/*.ts", - "../../libs/common/src/platform/services/**/*.worker.ts" + "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts" ] } diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json index 13a6466b3b5..1c9a530d273 100644 --- a/bitwarden_license/bit-web/tsconfig.json +++ b/bitwarden_license/bit-web/tsconfig.json @@ -21,10 +21,10 @@ "../../libs/tools/export/vault-export/vault-export-core/src" ], "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], - "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/importer-core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/key-management": ["../../libs/key-management/src"], - "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], + "@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/ui-common": ["../../libs/ui/common/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], @@ -46,7 +46,7 @@ "../../apps/web/src/connectors/*.ts", "../../apps/web/src/**/*.stories.ts", "../../apps/web/src/**/*.spec.ts", - "../../libs/common/src/platform/services/**/*.worker.ts", + "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts", "src/**/*.stories.ts", "src/**/*.spec.ts" diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000000..2d7c91521f9 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,381 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import angular from "angular-eslint"; +// @ts-ignore +import importPlugin from "eslint-plugin-import"; +import eslintConfigPrettier from "eslint-config-prettier"; +import eslintPluginTailwindCSS from "eslint-plugin-tailwindcss"; +import rxjs from "eslint-plugin-rxjs"; +import angularRxjs from "eslint-plugin-rxjs-angular"; +import storybook from "eslint-plugin-storybook"; + +import platformPlugins from "./libs/eslint/platform/index.mjs"; + +export default tseslint.config( + ...storybook.configs["flat/recommended"], + { + // Everything in this config object targets our TypeScript files (Components, Directives, Pipes etc) + files: ["**/*.ts", "**/*.js"], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + //...tseslint.configs.stylistic, + ...angular.configs.tsRecommended, + importPlugin.flatConfigs.recommended, + importPlugin.flatConfigs.typescript, + eslintConfigPrettier, // Disables rules that conflict with Prettier + ], + plugins: { + rxjs: rxjs, + "rxjs-angular": angularRxjs, + "@bitwarden/platform": platformPlugins, + }, + languageOptions: { + parserOptions: { + project: ["./tsconfig.eslint.json"], + sourceType: "module", + ecmaVersion: 2020, + }, + }, + settings: { + "import/parsers": { + "@typescript-eslint/parser": [".ts"], + }, + "import/resolver": { + typescript: { + alwaysTryTypes: true, + }, + }, + }, + processor: angular.processInlineTemplates, + rules: { + ...rxjs.configs.recommended.rules, + "rxjs-angular/prefer-takeuntil": ["error", { alias: ["takeUntilDestroyed"] }], + "rxjs/no-exposed-subjects": ["error", { allowProtected: true }], + + // TODO: Enable these. + "@angular-eslint/component-class-suffix": 0, + "@angular-eslint/contextual-lifecycle": 0, + "@angular-eslint/directive-class-suffix": 0, + "@angular-eslint/no-empty-lifecycle-method": 0, + "@angular-eslint/no-host-metadata-property": 0, + "@angular-eslint/no-input-rename": 0, + "@angular-eslint/no-inputs-metadata-property": 0, + "@angular-eslint/no-output-native": 0, + "@angular-eslint/no-output-on-prefix": 0, + "@angular-eslint/no-output-rename": 0, + "@angular-eslint/no-outputs-metadata-property": 0, + "@angular-eslint/use-lifecycle-interface": "error", + "@angular-eslint/use-pipe-transform-interface": 0, + "@bitwarden/platform/required-using": "error", + "@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }], + "@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }], + "@typescript-eslint/no-this-alias": ["error", { allowedNames: ["self"] }], + "@typescript-eslint/no-unused-expressions": ["error", { allowTernary: true }], + "@typescript-eslint/no-unused-vars": ["error", { args: "none" }], + + curly: ["error", "all"], + "no-console": "error", + + "import/order": [ + "error", + { + alphabetize: { + order: "asc", + }, + "newlines-between": "always", + pathGroups: [ + { + pattern: "@bitwarden/**", + group: "external", + position: "after", + }, + { + pattern: "src/**/*", + group: "parent", + position: "before", + }, + ], + pathGroupsExcludedImportTypes: ["builtin"], + }, + ], + "import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway + "import/no-restricted-paths": [ + "error", + { + zones: [ + { + target: ["libs/**/*"], + from: ["apps/**/*"], + message: "Libs should not import app-specific code.", + }, + { + // avoid specific frameworks or large dependencies in common + target: "./libs/common/**/*", + from: [ + // Angular + "./libs/angular/**/*", + "./node_modules/@angular*/**/*", + + // Node + "./libs/node/**/*", + + //Generator + "./libs/tools/generator/components/**/*", + "./libs/tools/generator/core/**/*", + "./libs/tools/generator/extensions/**/*", + + // Import/export + "./libs/importer/**/*", + "./libs/tools/export/vault-export/vault-export-core/**/*", + ], + }, + { + // avoid import of unexported state objects + target: [ + "!(libs)/**/*", + "libs/!(common)/**/*", + "libs/common/!(src)/**/*", + "libs/common/src/!(platform)/**/*", + "libs/common/src/platform/!(state)/**/*", + ], + from: ["./libs/common/src/platform/state/**/*"], + // allow module index import + except: ["**/state/index.ts"], + }, + ], + }, + ], + "import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package., + }, + }, + { + // Everything in this config object targets our HTML files (external templates, + // and inline templates as long as we have the `processor` set on our TypeScript config above) + files: ["**/*.html"], + extends: [ + // Apply the recommended Angular template rules + // ...angular.configs.templateRecommended, + // Apply the Angular template rules which focus on accessibility of our apps + // ...angular.configs.templateAccessibility, + ], + languageOptions: { + parser: angular.templateParser, + }, + plugins: { + "@angular-eslint/template": angular.templatePlugin, + tailwindcss: eslintPluginTailwindCSS, + }, + rules: { + "@angular-eslint/template/button-has-type": "error", + "tailwindcss/no-custom-classname": [ + "error", + { + // uses negative lookahead to whitelist any class that doesn't start with "tw-" + // in other words: classnames that start with tw- must be valid TailwindCSS classes + whitelist: ["(?!(tw)\\-).*"], + }, + ], + "tailwindcss/enforces-negative-arbitrary-values": "error", + "tailwindcss/enforces-shorthand": "error", + "tailwindcss/no-contradicting-classname": "error", + }, + }, + + // Global quirks + { + files: ["apps/browser/src/**/*.ts", "libs/**/*.ts"], + ignores: [ + "apps/browser/src/autofill/{deprecated/content,content,notification}/**/*.ts", + "apps/browser/src/**/background/**/*.ts", // It's okay to have long lived listeners in the background + "apps/browser/src/platform/background.ts", + ], + rules: { + "no-restricted-syntax": [ + "error", + { + message: + "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead", + // This selector covers events like chrome.storage.onChange & chrome.runtime.onMessage + selector: + "CallExpression > [object.object.object.name='chrome'][property.name='addListener']", + }, + { + message: + "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead", + // This selector covers events like chrome.storage.local.onChange + selector: + "CallExpression > [object.object.object.object.name='chrome'][property.name='addListener']", + }, + ], + }, + }, + { + files: ["**/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports(), + }, + }, + + // App overrides. Be considerate if you override these. + { + files: ["apps/browser/src/**/*.ts"], + ignores: [ + "apps/browser/src/**/{content,popup,spec}/**/*.ts", + "apps/browser/src/**/autofill/{notification,overlay}/**/*.ts", + "apps/browser/src/**/autofill/**/{autofill-overlay-content,collect-autofill-content,dom-element-visibility,insert-autofill-content}.service.ts", + "apps/browser/src/**/*.spec.ts", + ], + rules: { + "no-restricted-globals": [ + "error", + { + name: "window", + message: + "The `window` object is not available in service workers and may not be available within the background script. Consider using `self`, `globalThis`, or another global property instead.", + }, + ], + }, + }, + { + files: ["bitwarden_license/bit-common/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports(["@bitwarden/bit-common/*"]), + }, + }, + { + files: ["apps/**/*.ts"], + rules: { + // Catches static imports + "no-restricted-imports": buildNoRestrictedImports([ + "bitwarden_license/**", + "@bitwarden/bit-common/*", + "@bitwarden/bit-web/*", + ]), + }, + }, + { + files: ["apps/web/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + "bitwarden_license/**", + "@bitwarden/bit-common/*", + "@bitwarden/bit-web/*", + + "**/app/core/*", + "**/reports/*", + "**/app/shared/*", + "**/organizations/settings/*", + "**/organizations/policies/*", + ]), + }, + }, + + /// Team overrides + { + files: ["**/src/platform/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([], true), + }, + }, + { + files: [ + "apps/cli/src/admin-console/**/*.ts", + "apps/web/src/app/admin-console/**/*.ts", + "bitwarden_license/bit-cli/src/admin-console/**/*.ts", + "bitwarden_license/bit-web/src/app/admin-console/**/*.ts", + "libs/admin-console/src/**/*.ts", + ], + rules: { + "@angular-eslint/component-class-suffix": "error", + "@angular-eslint/contextual-lifecycle": "error", + "@angular-eslint/directive-class-suffix": "error", + "@angular-eslint/no-empty-lifecycle-method": "error", + "@angular-eslint/no-input-rename": "error", + "@angular-eslint/no-inputs-metadata-property": "error", + "@angular-eslint/no-output-native": "error", + "@angular-eslint/no-output-on-prefix": "error", + "@angular-eslint/no-output-rename": "error", + "@angular-eslint/no-outputs-metadata-property": "error", + "@angular-eslint/use-lifecycle-interface": "error", + "@angular-eslint/use-pipe-transform-interface": "error", + }, + }, + { + files: ["libs/common/src/state-migrations/**/*.ts"], + rules: { + "import/no-restricted-paths": [ + "error", + { + basePath: "libs/common/src/state-migrations", + zones: [ + { + target: "./", + from: "../", + // Relative to from, not basePath + except: ["state-migrations"], + message: + "State migrations should rarely import from the greater codebase. If you need to import from another location, take into account the likelihood of change in that code and consider copying to the migration instead.", + }, + ], + }, + ], + }, + }, + + // Keep ignores at the end + { + ignores: [ + "**/build/", + "**/dist/", + "**/coverage/", + ".angular/", + "storybook-static/", + + "**/node_modules/", + + "**/webpack.*.js", + "**/jest.config.js", + + "apps/browser/config/config.js", + "apps/browser/src/auth/scripts/duo.js", + "apps/browser/webpack/manifest.js", + + "apps/desktop/desktop_native", + "apps/desktop/src/auth/scripts/duo.js", + + "apps/web/config.js", + "apps/web/scripts/*.js", + "apps/web/tailwind.config.js", + + "apps/cli/config/config.js", + + "tailwind.config.js", + "libs/components/tailwind.config.base.js", + "libs/components/tailwind.config.js", + + "scripts/*.js", + ], + }, +); + +/** + * // Helper function for building no-restricted-imports rule + * @param {string[]} additionalForbiddenPatterns + * @returns {any} + */ +function buildNoRestrictedImports(additionalForbiddenPatterns = [], skipPlatform = false) { + return [ + "error", + { + patterns: [ + ...(skipPlatform ? [] : ["**/platform/**/internal", "**/platform/messaging/**"]), + "**/src/**/*", // Prevent relative imports across libs. + ].concat(additionalForbiddenPatterns), + }, + ]; +} diff --git a/jest.config.js b/jest.config.js index 3ed082bcbc3..e8815f92ffb 100644 --- a/jest.config.js +++ b/jest.config.js @@ -30,6 +30,7 @@ module.exports = { "/libs/billing/jest.config.js", "/libs/common/jest.config.js", "/libs/components/jest.config.js", + "/libs/eslint/jest.config.js", "/libs/tools/export/vault-export/vault-export-core/jest.config.js", "/libs/tools/generator/core/jest.config.js", "/libs/tools/generator/components/jest.config.js", @@ -42,6 +43,7 @@ module.exports = { "/libs/node/jest.config.js", "/libs/vault/jest.config.js", "/libs/key-management/jest.config.js", + "/libs/key-management-ui/jest.config.js", ], // Workaround for a memory leak that crashes tests in CI: diff --git a/libs/admin-console/.eslintrc.json b/libs/admin-console/.eslintrc.json deleted file mode 100644 index d8aa8f64a88..00000000000 --- a/libs/admin-console/.eslintrc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "overrides": [ - { - "files": ["*.ts"], - "extends": ["plugin:@angular-eslint/recommended"], - "rules": { - "@angular-eslint/component-class-suffix": "error", - "@angular-eslint/contextual-lifecycle": "error", - "@angular-eslint/directive-class-suffix": "error", - "@angular-eslint/no-empty-lifecycle-method": "error", - "@angular-eslint/no-input-rename": "error", - "@angular-eslint/no-inputs-metadata-property": "error", - "@angular-eslint/no-output-native": "error", - "@angular-eslint/no-output-on-prefix": "error", - "@angular-eslint/no-output-rename": "error", - "@angular-eslint/no-outputs-metadata-property": "error", - "@angular-eslint/use-lifecycle-interface": "error", - "@angular-eslint/use-pipe-transform-interface": "error" - } - } - ] -} diff --git a/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts index 6aafbaf4678..890353d9039 100644 --- a/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { KeyService } from "@bitwarden/key-management"; diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts index a230a20b2e3..7fe81ade4d2 100644 --- a/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts @@ -1,7 +1,7 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.ts b/libs/admin-console/src/common/collections/services/default-collection.service.ts index 4070c92f27c..da50a25886e 100644 --- a/libs/admin-console/src/common/collections/services/default-collection.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection.service.ts @@ -3,7 +3,7 @@ import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; import { Jsonify } from "type-fest"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts index 048a4733948..9700fcb695a 100644 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { first, firstValueFrom, of, ReplaySubject, takeWhile } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts index 2d5a083592b..0ef8ae99ab3 100644 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts +++ b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { combineLatest, filter, firstValueFrom, map } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { StateProvider, DerivedState } from "@bitwarden/common/platform/state"; diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 0b19935985a..52a22ac2946 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -63,7 +63,15 @@ export class CollectionsComponent implements OnInit { } if (this.organization == null) { - this.organization = await this.organizationService.get(this.cipher.organizationId); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(activeUserId) + .pipe( + map((organizations) => + organizations.find((org) => org.id === this.cipher.organizationId), + ), + ), + ); } } diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts b/libs/angular/src/auth/components/authentication-timeout.component.ts similarity index 89% rename from libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts rename to libs/angular/src/auth/components/authentication-timeout.component.ts index faa08cf073b..1a5d398a291 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-expired.component.ts +++ b/libs/angular/src/auth/components/authentication-timeout.component.ts @@ -10,7 +10,7 @@ import { ButtonModule } from "@bitwarden/components"; * It provides a button to navigate to the login page. */ @Component({ - selector: "app-two-factor-expired", + selector: "app-authentication-timeout", standalone: true, imports: [CommonModule, JslibModule, ButtonModule, RouterModule], template: ` @@ -22,4 +22,4 @@ import { ButtonModule } from "@bitwarden/components"; `, }) -export class TwoFactorTimeoutComponent {} +export class AuthenticationTimeoutComponent {} diff --git a/libs/angular/src/auth/components/base-login-decryption-options-v1.component.ts b/libs/angular/src/auth/components/base-login-decryption-options-v1.component.ts index ca3906cead3..32396c878d9 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options-v1.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options-v1.component.ts @@ -195,7 +195,7 @@ export class BaseLoginDecryptionOptionsComponentV1 implements OnInit, OnDestroy async loadNewUserData() { const autoEnrollStatus$ = defer(() => - this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(), + this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId), ).pipe( switchMap((organizationIdentifier) => { if (organizationIdentifier == undefined) { diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index 7f54f35cb2a..ea2f9695768 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -10,12 +10,10 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management"; import { PasswordColorText } from "../../tools/password-strength/password-strength.component"; @@ -41,10 +39,8 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { protected i18nService: I18nService, protected keyService: KeyService, protected messagingService: MessagingService, - protected passwordGenerationService: PasswordGenerationServiceAbstraction, protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService, - protected stateService: StateService, protected dialogService: DialogService, protected kdfConfigService: KdfConfigService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, diff --git a/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts b/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts index 386068ff783..7409acf6845 100644 --- a/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request-v1.component.ts @@ -64,11 +64,12 @@ export class LoginViaAuthRequestComponentV1 protected StateEnum = State; protected state = State.StandardAuthRequest; - + protected webVaultUrl: string; protected twoFactorRoute = "2fa"; protected successRoute = "vault"; protected forcePasswordResetRoute = "update-temp-password"; private resendTimeout = 12000; + protected deviceManagementUrl: string; private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array }; @@ -95,6 +96,12 @@ export class LoginViaAuthRequestComponentV1 ) { super(environmentService, i18nService, platformUtilsService, toastService); + // Get the web vault URL from the environment service + environmentService.environment$.pipe(takeUntil(this.destroy$)).subscribe((env) => { + this.webVaultUrl = env.getWebVaultUrl(); + this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`; + }); + // Gets signalR push notification // Only fires on approval to prevent enumeration this.authRequestService.authRequestPushNotification$ diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index 279294f4c06..e4787aa8c01 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -15,10 +15,8 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; import { @@ -91,9 +89,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn i18nService: I18nService, protected keyService: KeyService, protected apiService: ApiService, - protected stateService: StateService, platformUtilsService: PlatformUtilsService, - protected passwordGenerationService: PasswordGenerationServiceAbstraction, environmentService: EnvironmentService, protected logService: LogService, protected auditService: AuditService, diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index 166707a19ea..de079a7ebca 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -21,12 +21,11 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -34,7 +33,6 @@ import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -49,7 +47,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements resetPasswordAutoEnroll = false; onSuccessfulChangePassword: () => Promise; successRoute = "vault"; - userId: UserId; + activeUserId: UserId; forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None; ForceSetPasswordReason = ForceSetPasswordReason; @@ -60,7 +58,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements i18nService: I18nService, keyService: KeyService, messagingService: MessagingService, - passwordGenerationService: PasswordGenerationServiceAbstraction, platformUtilsService: PlatformUtilsService, private policyApiService: PolicyApiServiceAbstraction, policyService: PolicyService, @@ -68,7 +65,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements private apiService: ApiService, private syncService: SyncService, private route: ActivatedRoute, - stateService: StateService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserApiService: OrganizationUserApiService, private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, @@ -82,10 +78,8 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements i18nService, keyService, messagingService, - passwordGenerationService, platformUtilsService, policyService, - stateService, dialogService, kdfConfigService, masterPasswordService, @@ -102,10 +96,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements await this.syncService.fullSync(true); this.syncLoading = false; - this.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; this.forceSetPasswordReason = await firstValueFrom( - this.masterPasswordService.forceSetPasswordReason$(this.userId), + this.masterPasswordService.forceSetPasswordReason$(this.activeUserId), ); this.route.queryParams @@ -117,7 +111,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements } else { // Try to get orgSsoId from state as fallback // Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario. - return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(); + return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId); } }), filter((orgSsoId) => orgSsoId != null), @@ -173,10 +167,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements // in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one const existingUserPrivateKey = (await firstValueFrom( - this.keyService.userPrivateKey$(this.userId), + this.keyService.userPrivateKey$(this.activeUserId), )) as Uint8Array; const existingUserPublicKey = await firstValueFrom( - this.keyService.userPublicKey$(this.userId), + this.keyService.userPublicKey$(this.activeUserId), ); if (existingUserPrivateKey != null && existingUserPublicKey != null) { const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey); @@ -223,7 +217,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment( this.orgId, - this.userId, + this.activeUserId, resetRequest, ); }); @@ -266,7 +260,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements // Clear force set password reason to allow navigation back to vault. await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.None, - this.userId, + this.activeUserId, ); // User now has a password so update account decryption options in state @@ -275,9 +269,9 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements ); userDecryptionOpts.hasMasterPassword = true; await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); - await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig); - await this.masterPasswordService.setMasterKey(masterKey, this.userId); - await this.keyService.setUserKey(userKey[0], this.userId); + await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig); + await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId); + await this.keyService.setUserKey(userKey[0], this.activeUserId); // Set private key only for new JIT provisioned users in MP encryption orgs // Existing TDE users will have private key set on sync or on login @@ -286,7 +280,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements this.forceSetPasswordReason != ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission ) { - await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.userId); + await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId); } const localMasterKeyHash = await this.keyService.hashMasterKey( @@ -294,6 +288,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements masterKey, HashPurpose.LocalAuthorization, ); - await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.userId); + await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId); } } diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 6c13809566a..d0fc2140f06 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, NavigationExtras, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; @@ -27,6 +28,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @@ -55,6 +57,7 @@ export class SsoComponent implements OnInit { protected redirectUri: string; protected state: string; protected codeChallenge: string; + protected activeUserId: UserId; constructor( protected ssoLoginService: SsoLoginServiceAbstraction, @@ -74,7 +77,11 @@ export class SsoComponent implements OnInit { protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected accountService: AccountService, protected toastService: ToastService, - ) {} + ) { + this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => { + this.activeUserId = account?.id; + }); + } async ngOnInit() { // eslint-disable-next-line rxjs/no-async-subscribe @@ -226,7 +233,10 @@ export class SsoComponent implements OnInit { // - TDE login decryption options component // - Browser SSO on extension open // Note: you cannot set this in state before 2FA b/c there won't be an account in state. - await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier); + await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier( + orgSsoIdentifier, + this.activeUserId, + ); // Users enrolled in admin acct recovery can be forced to set a new password after // having the admin set a temp password for them (affects TDE & standard users) diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html index 8462a18ac2e..087ecd2764e 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html @@ -69,7 +69,7 @@
diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts index 6aca189a79e..6afee461c42 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, Inject, OnInit, ViewChild } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; import { ActivatedRoute, NavigationExtras, Router, RouterLink } from "@angular/router"; import { Subject, takeUntil, lastValueFrom, first, firstValueFrom } from "rxjs"; @@ -31,6 +32,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { AsyncActionsModule, ButtonModule, @@ -126,6 +128,7 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements protected changePasswordRoute = "set-password"; protected forcePasswordResetRoute = "update-temp-password"; protected successRoute = "vault"; + protected activeUserId: UserId; constructor( protected loginStrategyService: LoginStrategyServiceAbstraction, @@ -148,6 +151,10 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements protected toastService: ToastService, ) { super(environmentService, i18nService, platformUtilsService, toastService); + + this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => { + this.activeUserId = account?.id; + }); } async ngOnInit() { @@ -214,7 +221,7 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements } } - async selectOtherTwofactorMethod() { + async selectOtherTwoFactorMethod() { const dialogRef = TwoFactorOptionsComponent.open(this.dialogService); const response: TwoFactorOptionsDialogResultType = await lastValueFrom(dialogRef.closed); if (response.result === TwoFactorOptionsDialogResult.Provider) { @@ -262,7 +269,10 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements // Save off the OrgSsoIdentifier for use in the TDE flows // - TDE login decryption options component // - Browser SSO on extension open - await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier); + await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier( + this.orgIdentifier, + this.activeUserId, + ); this.loginEmailService.clearValues(); // note: this flow affects both TDE & standard users diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index 5a1903d6671..414aa1dc2a3 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -86,12 +86,12 @@ describe("TwoFactorComponent", () => { }; let selectedUserDecryptionOptions: BehaviorSubject; - let twoFactorTimeoutSubject: BehaviorSubject; + let authenticationSessionTimeoutSubject: BehaviorSubject; beforeEach(() => { - twoFactorTimeoutSubject = new BehaviorSubject(false); + authenticationSessionTimeoutSubject = new BehaviorSubject(false); mockLoginStrategyService = mock(); - mockLoginStrategyService.twoFactorTimeout$ = twoFactorTimeoutSubject; + mockLoginStrategyService.authenticationSessionTimeout$ = authenticationSessionTimeoutSubject; mockRouter = mock(); mockI18nService = mock(); mockApiService = mock(); @@ -153,7 +153,9 @@ describe("TwoFactorComponent", () => { }), }; - selectedUserDecryptionOptions = new BehaviorSubject(null); + selectedUserDecryptionOptions = new BehaviorSubject( + mockUserDecryptionOpts.withMasterPassword, + ); mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions; TestBed.configureTestingModule({ @@ -497,8 +499,8 @@ describe("TwoFactorComponent", () => { }); it("navigates to the timeout route when timeout expires", async () => { - twoFactorTimeoutSubject.next(true); + authenticationSessionTimeoutSubject.next(true); - expect(mockRouter.navigate).toHaveBeenCalledWith(["2fa-timeout"]); + expect(mockRouter.navigate).toHaveBeenCalledWith(["authentication-timeout"]); }); }); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index e2b41ad086d..49af9d057f7 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -35,6 +35,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { CaptchaProtectedComponent } from "./captcha-protected.component"; @@ -71,7 +72,9 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected changePasswordRoute = "set-password"; protected forcePasswordResetRoute = "update-temp-password"; protected successRoute = "vault"; - protected twoFactorTimeoutRoute = "2fa-timeout"; + protected twoFactorTimeoutRoute = "authentication-timeout"; + + protected activeUserId: UserId; get isDuoProvider(): boolean { return ( @@ -102,10 +105,15 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected toastService: ToastService, ) { super(environmentService, i18nService, platformUtilsService, toastService); + this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); - // Add subscription to twoFactorTimeout$ and navigate to twoFactorTimeoutRoute if expired - this.loginStrategyService.twoFactorTimeout$ + this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => { + this.activeUserId = account?.id; + }); + + // Add subscription to authenticationSessionTimeout$ and navigate to twoFactorTimeoutRoute if expired + this.loginStrategyService.authenticationSessionTimeout$ .pipe(takeUntilDestroyed()) .subscribe(async (expired) => { if (!expired) { @@ -287,7 +295,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI // Save off the OrgSsoIdentifier for use in the TDE flows // - TDE login decryption options component // - Browser SSO on extension open - await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier); + await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier( + this.orgIdentifier, + this.activeUserId, + ); this.loginEmailService.clearValues(); // note: this flow affects both TDE & standard users diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 1d1057d9aa6..e6cefd40d1d 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -16,11 +16,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -39,12 +37,10 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { protected router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationServiceAbstraction, policyService: PolicyService, keyService: KeyService, messagingService: MessagingService, private apiService: ApiService, - stateService: StateService, private userVerificationService: UserVerificationService, private logService: LogService, dialogService: DialogService, @@ -57,10 +53,8 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { i18nService, keyService, messagingService, - passwordGenerationService, platformUtilsService, policyService, - stateService, dialogService, kdfConfigService, masterPasswordService, diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index 3fb1f7400ec..95c56d08486 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -20,12 +20,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -51,12 +49,10 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp constructor( i18nService: I18nService, platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationServiceAbstraction, policyService: PolicyService, keyService: KeyService, messagingService: MessagingService, private apiService: ApiService, - stateService: StateService, private syncService: SyncService, private logService: LogService, private userVerificationService: UserVerificationService, @@ -71,10 +67,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp i18nService, keyService, messagingService, - passwordGenerationService, platformUtilsService, policyService, - stateService, dialogService, kdfConfigService, masterPasswordService, diff --git a/libs/angular/src/auth/guards/active-auth.guard.spec.ts b/libs/angular/src/auth/guards/active-auth.guard.spec.ts new file mode 100644 index 00000000000..c3417b9d41d --- /dev/null +++ b/libs/angular/src/auth/guards/active-auth.guard.spec.ts @@ -0,0 +1,71 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { activeAuthGuard } from "./active-auth.guard"; + +@Component({ template: "" }) +class EmptyComponent {} + +describe("activeAuthGuard", () => { + const setup = (authType: AuthenticationType | null) => { + const loginStrategyService: MockProxy = + mock(); + const currentAuthTypeSubject = new BehaviorSubject(authType); + loginStrategyService.currentAuthType$ = currentAuthTypeSubject; + + const logService: MockProxy = mock(); + + const testBed = TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { path: "", component: EmptyComponent }, + { + path: "protected-route", + component: EmptyComponent, + canActivate: [activeAuthGuard()], + }, + { path: "login", component: EmptyComponent }, + ]), + ], + providers: [ + { provide: LoginStrategyServiceAbstraction, useValue: loginStrategyService }, + { provide: LogService, useValue: logService }, + ], + declarations: [EmptyComponent], + }); + + return { + router: testBed.inject(Router), + logService, + loginStrategyService, + }; + }; + + it("creates the guard", () => { + const { router } = setup(AuthenticationType.Password); + expect(router).toBeTruthy(); + }); + + it("allows access with an active login session", async () => { + const { router } = setup(AuthenticationType.Password); + + await router.navigate(["protected-route"]); + expect(router.url).toBe("/protected-route"); + }); + + it("redirects to login with no active session", async () => { + const { router, logService } = setup(null); + + await router.navigate(["protected-route"]); + expect(router.url).toBe("/login"); + expect(logService.error).toHaveBeenCalledWith("No active login session found."); + }); +}); diff --git a/libs/angular/src/auth/guards/active-auth.guard.ts b/libs/angular/src/auth/guards/active-auth.guard.ts new file mode 100644 index 00000000000..56213bbd979 --- /dev/null +++ b/libs/angular/src/auth/guards/active-auth.guard.ts @@ -0,0 +1,28 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +/** + * Guard that ensures there is an active login session before allowing access + * to the new device verification route. + * If not, redirects to login. + */ +export function activeAuthGuard(): CanActivateFn { + return async () => { + const loginStrategyService = inject(LoginStrategyServiceAbstraction); + const logService = inject(LogService); + const router = inject(Router); + + // Check if we have a valid login session + const authType = await firstValueFrom(loginStrategyService.currentAuthType$); + if (authType === null) { + logService.error("No active login session found."); + return router.createUrlTree(["/login"]); + } + + return true; + }; +} diff --git a/libs/angular/src/auth/guards/index.ts b/libs/angular/src/auth/guards/index.ts index 1760a870b3a..026848c4b08 100644 --- a/libs/angular/src/auth/guards/index.ts +++ b/libs/angular/src/auth/guards/index.ts @@ -1,4 +1,5 @@ export * from "./auth.guard"; +export * from "./active-auth.guard"; export * from "./lock.guard"; export * from "./redirect.guard"; export * from "./tde-decryption-required.guard"; diff --git a/libs/angular/src/auth/guards/unauth.guard.spec.ts b/libs/angular/src/auth/guards/unauth.guard.spec.ts index 6d8619f4d43..ec36b146a03 100644 --- a/libs/angular/src/auth/guards/unauth.guard.spec.ts +++ b/libs/angular/src/auth/guards/unauth.guard.spec.ts @@ -5,17 +5,48 @@ import { MockProxy, mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { unauthGuardFn } from "./unauth.guard"; describe("UnauthGuard", () => { - const setup = (authStatus: AuthenticationStatus) => { + const activeUser: Account = { + id: "fake_user_id" as UserId, + email: "test@email.com", + emailVerified: true, + name: "Test User", + }; + + const setup = ( + activeUser: Account | null, + authStatus: AuthenticationStatus | null = null, + tdeEnabled: boolean = false, + everHadUserKey: boolean = false, + ) => { + const accountService: MockProxy = mock(); const authService: MockProxy = mock(); - authService.getAuthStatus.mockResolvedValue(authStatus); - const activeAccountStatusObservable = new BehaviorSubject(authStatus); - authService.activeAccountStatus$ = activeAccountStatusObservable; + const keyService: MockProxy = mock(); + const deviceTrustService: MockProxy = + mock(); + const logService: MockProxy = mock(); + + accountService.activeAccount$ = new BehaviorSubject(activeUser); + + if (authStatus !== null) { + const activeAccountStatusObservable = new BehaviorSubject(authStatus); + authService.authStatusFor$.mockReturnValue(activeAccountStatusObservable); + } + + keyService.everHadUserKey$ = new BehaviorSubject(everHadUserKey); + deviceTrustService.supportsDeviceTrustByUserId$.mockReturnValue( + new BehaviorSubject(tdeEnabled), + ); const testBed = TestBed.configureTestingModule({ imports: [ @@ -30,6 +61,7 @@ describe("UnauthGuard", () => { { path: "lock", component: EmptyComponent }, { path: "testhomepage", component: EmptyComponent }, { path: "testlocked", component: EmptyComponent }, + { path: "login-initiated", component: EmptyComponent }, { path: "testOverrides", component: EmptyComponent, @@ -39,7 +71,13 @@ describe("UnauthGuard", () => { }, ]), ], - providers: [{ provide: AuthService, useValue: authService }], + providers: [ + { provide: AccountService, useValue: accountService }, + { provide: AuthService, useValue: authService }, + { provide: KeyService, useValue: keyService }, + { provide: DeviceTrustServiceAbstraction, useValue: deviceTrustService }, + { provide: LogService, useValue: logService }, + ], }); return { @@ -48,40 +86,54 @@ describe("UnauthGuard", () => { }; it("should be created", () => { - const { router } = setup(AuthenticationStatus.LoggedOut); + const { router } = setup(null, AuthenticationStatus.LoggedOut); expect(router).toBeTruthy(); }); it("should redirect to /vault for guarded routes when logged in and unlocked", async () => { - const { router } = setup(AuthenticationStatus.Unlocked); + const { router } = setup(activeUser, AuthenticationStatus.Unlocked); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/vault"); }); - it("should allow access to guarded routes when logged out", async () => { - const { router } = setup(AuthenticationStatus.LoggedOut); + it("should allow access to guarded routes when account is null", async () => { + const { router } = setup(null); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/unauth-guarded-route"); }); + it("should allow access to guarded routes when logged out", async () => { + const { router } = setup(null, AuthenticationStatus.LoggedOut); + + await router.navigateByUrl("unauth-guarded-route"); + expect(router.url).toBe("/unauth-guarded-route"); + }); + + it("should redirect to /login-initiated when locked, TDE is enabled, and the user hasn't decrypted yet", async () => { + const { router } = setup(activeUser, AuthenticationStatus.Locked, true, false); + + await router.navigateByUrl("unauth-guarded-route"); + expect(router.url).toBe("/login-initiated"); + }); + it("should redirect to /lock for guarded routes when locked", async () => { - const { router } = setup(AuthenticationStatus.Locked); + const { router } = setup(activeUser, AuthenticationStatus.Locked); await router.navigateByUrl("unauth-guarded-route"); expect(router.url).toBe("/lock"); }); it("should redirect to /testhomepage for guarded routes when testOverrides are provided and the account is unlocked", async () => { - const { router } = setup(AuthenticationStatus.Unlocked); + const { router } = setup(activeUser, AuthenticationStatus.Unlocked); await router.navigateByUrl("testOverrides"); expect(router.url).toBe("/testhomepage"); }); it("should redirect to /testlocked for guarded routes when testOverrides are provided and the account is locked", async () => { - const { router } = setup(AuthenticationStatus.Locked); + const { router } = setup(activeUser, AuthenticationStatus.Locked); await router.navigateByUrl("testOverrides"); expect(router.url).toBe("/testlocked"); diff --git a/libs/angular/src/auth/guards/unauth.guard.ts b/libs/angular/src/auth/guards/unauth.guard.ts index f96668773ef..1ac0eebb458 100644 --- a/libs/angular/src/auth/guards/unauth.guard.ts +++ b/libs/angular/src/auth/guards/unauth.guard.ts @@ -1,9 +1,13 @@ import { inject } from "@angular/core"; -import { CanActivateFn, Router, UrlTree } from "@angular/router"; -import { Observable, map } from "rxjs"; +import { ActivatedRouteSnapshot, CanActivateFn, Router, UrlTree } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { KeyService } from "@bitwarden/key-management"; type UnauthRoutes = { homepage: () => string; @@ -15,23 +19,54 @@ const defaultRoutes: UnauthRoutes = { locked: "/lock", }; -function unauthGuard(routes: UnauthRoutes): Observable { +// TODO: PM-17195 - Investigate consolidating unauthGuard and redirectGuard into AuthStatusGuard +async function unauthGuard( + route: ActivatedRouteSnapshot, + routes: UnauthRoutes, +): Promise { + const accountService = inject(AccountService); const authService = inject(AuthService); const router = inject(Router); + const keyService = inject(KeyService); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); + const logService = inject(LogService); - return authService.activeAccountStatus$.pipe( - map((status) => { - if (status == null || status === AuthenticationStatus.LoggedOut) { - return true; - } else if (status === AuthenticationStatus.Locked) { - return router.createUrlTree([routes.locked]); - } else { - return router.createUrlTree([routes.homepage()]); - } - }), + const activeUser = await firstValueFrom(accountService.activeAccount$); + + if (!activeUser) { + return true; + } + + const authStatus = await firstValueFrom(authService.authStatusFor$(activeUser.id)); + + if (authStatus == null || authStatus === AuthenticationStatus.LoggedOut) { + return true; + } + + if (authStatus === AuthenticationStatus.Unlocked) { + return router.createUrlTree([routes.homepage()]); + } + + const tdeEnabled = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(activeUser.id), ); + const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$); + + // If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the + // login decryption options component. + if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) { + logService.info( + "Sending user to TDE decryption options. AuthStatus is %s. TDE support is %s. Ever had user key is %s.", + AuthenticationStatus[authStatus], + tdeEnabled, + everHadUserKey, + ); + return router.createUrlTree(["/login-initiated"]); + } + + return router.createUrlTree([routes.locked]); } export function unauthGuardFn(overrides: Partial = {}): CanActivateFn { - return () => unauthGuard({ ...defaultRoutes, ...overrides }); + return async (route) => unauthGuard(route, { ...defaultRoutes, ...overrides }); } diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts index cebd81846c1..edb233cc76e 100644 --- a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts +++ b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts @@ -3,14 +3,14 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AccountService, AccountInfo } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -105,7 +105,16 @@ export class AddAccountCreditDialogComponent implements OnInit { this.formGroup.patchValue({ creditAmount: 20.0, }); - this.organization = await this.organizationService.get(this.dialogParams.organizationId); + this.user = await firstValueFrom(this.accountService.activeAccount$); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(this.user.id) + .pipe( + map((organizations) => + organizations.find((org) => org.id === this.dialogParams.organizationId), + ), + ), + ); payPalCustomField = "organization_id:" + this.organization.id; this.payPalConfig.subject = this.organization.name; } else if (this.dialogParams.providerId) { @@ -119,7 +128,6 @@ export class AddAccountCreditDialogComponent implements OnInit { this.formGroup.patchValue({ creditAmount: 10.0, }); - this.user = await firstValueFrom(this.accountService.activeAccount$); payPalCustomField = "user_id:" + this.user.id; this.payPalConfig.subject = this.user.email; } diff --git a/libs/angular/src/billing/components/index.ts b/libs/angular/src/billing/components/index.ts index 675d7555ed2..dacb5b265bd 100644 --- a/libs/angular/src/billing/components/index.ts +++ b/libs/angular/src/billing/components/index.ts @@ -2,3 +2,4 @@ export * from "./add-account-credit-dialog/add-account-credit-dialog.component"; export * from "./invoices/invoices.component"; export * from "./invoices/no-invoices.component"; export * from "./manage-tax-information/manage-tax-information.component"; +export * from "./premium.component"; diff --git a/libs/angular/src/vault/components/premium.component.ts b/libs/angular/src/billing/components/premium.component.ts similarity index 82% rename from libs/angular/src/vault/components/premium.component.ts rename to libs/angular/src/billing/components/premium.component.ts index e86c6beda47..6d0b90385ba 100644 --- a/libs/angular/src/vault/components/premium.component.ts +++ b/libs/angular/src/billing/components/premium.component.ts @@ -6,8 +6,6 @@ import { firstValueFrom, Observable, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -20,13 +18,11 @@ export class PremiumComponent implements OnInit { price = 10; refreshPromise: Promise; cloudWebVaultUrl: string; - extensionRefreshFlagEnabled: boolean; constructor( protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected apiService: ApiService, - protected configService: ConfigService, private logService: LogService, protected dialogService: DialogService, private environmentService: EnvironmentService, @@ -43,9 +39,6 @@ export class PremiumComponent implements OnInit { async ngOnInit() { this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); - this.extensionRefreshFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); } async refresh() { @@ -66,15 +59,13 @@ export class PremiumComponent implements OnInit { const dialogOpts: SimpleDialogOptions = { title: { key: "continueToBitwardenDotCom" }, content: { - key: this.extensionRefreshFlagEnabled ? "premiumPurchaseAlertV2" : "premiumPurchaseAlert", + key: "premiumPurchaseAlertV2", }, type: "info", }; - if (this.extensionRefreshFlagEnabled) { - dialogOpts.acceptButtonText = { key: "continue" }; - dialogOpts.cancelButtonText = { key: "close" }; - } + dialogOpts.acceptButtonText = { key: "continue" }; + dialogOpts.cancelButtonText = { key: "close" }; const confirmed = await this.dialogService.openSimpleDialog(dialogOpts); diff --git a/libs/angular/src/directives/not-premium.directive.ts b/libs/angular/src/billing/directives/not-premium.directive.ts similarity index 100% rename from libs/angular/src/directives/not-premium.directive.ts rename to libs/angular/src/billing/directives/not-premium.directive.ts diff --git a/libs/angular/src/directives/premium.directive.ts b/libs/angular/src/billing/directives/premium.directive.ts similarity index 100% rename from libs/angular/src/directives/premium.directive.ts rename to libs/angular/src/billing/directives/premium.directive.ts diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 37dec53b9c7..534a1337eda 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -54,7 +54,11 @@ export class ShareComponent implements OnInit, OnDestroy { const allCollections = await this.collectionService.getAllDecrypted(); this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly); - this.organizations$ = this.organizationService.memberOrganizations$.pipe( + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + + this.organizations$ = this.organizationService.memberOrganizations$(userId).pipe( map((orgs) => { return orgs .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index b06faacef19..6ef2cf1d4da 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -30,6 +30,7 @@ import { } from "@bitwarden/components"; import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component"; +import { NotPremiumDirective } from "./billing/directives/not-premium.directive"; import { DeprecatedCalloutComponent } from "./components/callout.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; @@ -40,7 +41,6 @@ import { IfFeatureDirective } from "./directives/if-feature.directive"; import { InputStripSpacesDirective } from "./directives/input-strip-spaces.directive"; import { InputVerbatimDirective } from "./directives/input-verbatim.directive"; import { LaunchClickDirective } from "./directives/launch-click.directive"; -import { NotPremiumDirective } from "./directives/not-premium.directive"; import { StopClickDirective } from "./directives/stop-click.directive"; import { StopPropDirective } from "./directives/stop-prop.directive"; import { TextDragDirective } from "./directives/text-drag.directive"; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 7b6123e2589..719e3a084f1 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -39,13 +39,14 @@ import { DefaultAuthRequestApiService, DefaultLoginSuccessHandlerService, LoginSuccessHandlerService, + PasswordLoginStrategy, + PasswordLoginStrategyData, LoginApprovalComponentServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; -import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; @@ -67,8 +68,8 @@ import { } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrgDomainApiService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain-api.service"; import { OrgDomainService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain.service"; import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/services/organization-management-preferences/default-organization-management-preferences.service"; @@ -146,13 +147,15 @@ import { BillingApiService } from "@bitwarden/common/billing/services/billing-ap import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { TaxService } from "@bitwarden/common/billing/services/tax.service"; +import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation"; +import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService, RegionConfig, @@ -175,6 +178,16 @@ import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/inter import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { NotificationsService } from "@bitwarden/common/platform/notifications"; +// eslint-disable-next-line no-restricted-imports -- Needed for service creation +import { + DefaultNotificationsService, + NoopNotificationsService, + SignalRConnectionService, + UnsupportedWebPushConnectionService, + WebPushConnectionService, + WebPushNotificationsApiService, +} from "@bitwarden/common/platform/notifications/internal"; import { TaskSchedulerService, DefaultTaskSchedulerService, @@ -183,8 +196,6 @@ import { AppIdService } from "@bitwarden/common/platform/services/app-id.service import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; -import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation"; -import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service"; @@ -192,7 +203,6 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service"; import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; @@ -226,7 +236,6 @@ import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; -import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -273,12 +282,6 @@ import { PasswordGenerationServiceAbstraction, UsernameGenerationServiceAbstraction, } from "@bitwarden/generator-legacy"; -import { - ImportApiService, - ImportApiServiceAbstraction, - ImportService, - ImportServiceAbstraction, -} from "@bitwarden/importer/core"; import { KeyService, DefaultKeyService, @@ -293,7 +296,7 @@ import { DefaultUserAsymmetricKeysRegenerationApiService, } from "@bitwarden/key-management"; import { SafeInjectionToken } from "@bitwarden/ui-common"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { NewDeviceVerificationNoticeService, PasswordRepromptService } from "@bitwarden/vault"; import { VaultExportService, VaultExportServiceAbstraction, @@ -303,9 +306,6 @@ import { IndividualVaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { NewDeviceVerificationNoticeService } from "../../../vault/src/services/new-device-verification-notice.service"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; import { ViewCacheService } from "../platform/abstractions/view-cache.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; @@ -554,7 +554,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: InternalAccountService, useClass: AccountServiceImplementation, - deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider], + deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider, SingleUserStateProvider], }), safeProvider({ provide: AccountServiceAbstraction, @@ -796,7 +796,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: SsoLoginServiceAbstraction, useClass: SsoLoginService, - deps: [StateProvider], + deps: [StateProvider, LogService], }), safeProvider({ provide: STATE_FACTORY, @@ -817,26 +817,6 @@ const safeProviders: SafeProvider[] = [ MigrationRunner, ], }), - safeProvider({ - provide: ImportApiServiceAbstraction, - useClass: ImportApiService, - deps: [ApiServiceAbstraction], - }), - safeProvider({ - provide: ImportServiceAbstraction, - useClass: ImportService, - deps: [ - CipherServiceAbstraction, - FolderServiceAbstraction, - ImportApiServiceAbstraction, - I18nServiceAbstraction, - CollectionService, - KeyService, - EncryptService, - PinServiceAbstraction, - AccountServiceAbstraction, - ], - }), safeProvider({ provide: IndividualVaultExportServiceAbstraction, useClass: IndividualVaultExportService, @@ -877,19 +857,36 @@ const safeProviders: SafeProvider[] = [ deps: [LogService, I18nServiceAbstraction, StateProvider], }), safeProvider({ - provide: NotificationsServiceAbstraction, - useClass: devFlagEnabled("noopNotifications") ? NoopNotificationsService : NotificationsService, + provide: WebPushNotificationsApiService, + useClass: WebPushNotificationsApiService, + deps: [ApiServiceAbstraction, AppIdServiceAbstraction], + }), + safeProvider({ + provide: SignalRConnectionService, + useClass: SignalRConnectionService, + deps: [ApiServiceAbstraction, LogService], + }), + safeProvider({ + provide: WebPushConnectionService, + useClass: UnsupportedWebPushConnectionService, + deps: [], + }), + safeProvider({ + provide: NotificationsService, + useClass: devFlagEnabled("noopNotifications") + ? NoopNotificationsService + : DefaultNotificationsService, deps: [ LogService, SyncService, AppIdServiceAbstraction, - ApiServiceAbstraction, EnvironmentService, LOGOUT_CALLBACK, - StateServiceAbstraction, - AuthServiceAbstraction, MessagingServiceAbstraction, - TaskSchedulerService, + AccountServiceAbstraction, + SignalRConnectionService, + AuthServiceAbstraction, + WebPushConnectionService, ], }), safeProvider({ @@ -992,13 +989,14 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: InternalOrganizationServiceAbstraction, - useClass: OrganizationService, + useClass: DefaultOrganizationService, deps: [StateProvider], }), safeProvider({ provide: OrganizationServiceAbstraction, useExisting: InternalOrganizationServiceAbstraction, }), + safeProvider({ provide: OrganizationUserApiService, useClass: DefaultOrganizationUserApiService, @@ -1233,7 +1231,6 @@ const safeProviders: SafeProvider[] = [ deps: [ ApiServiceAbstraction, BillingApiServiceAbstraction, - ConfigService, KeyService, EncryptService, I18nServiceAbstraction, @@ -1394,7 +1391,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: CipherAuthorizationService, useClass: DefaultCipherAuthorizationService, - deps: [CollectionService, OrganizationServiceAbstraction], + deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction], }), safeProvider({ provide: AuthRequestApiService, @@ -1435,6 +1432,37 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultLoginSuccessHandlerService, deps: [SyncService, UserAsymmetricKeysRegenerationService], }), + safeProvider({ + provide: PasswordLoginStrategy, + useClass: PasswordLoginStrategy, + deps: [ + PasswordLoginStrategyData, + PasswordStrengthServiceAbstraction, + PolicyServiceAbstraction, + LoginStrategyServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, + KeyService, + EncryptService, + ApiServiceAbstraction, + TokenServiceAbstraction, + AppIdServiceAbstraction, + PlatformUtilsServiceAbstraction, + MessagingServiceAbstraction, + LogService, + StateServiceAbstraction, + TwoFactorServiceAbstraction, + InternalUserDecryptionOptionsServiceAbstraction, + BillingAccountProfileStateService, + VaultTimeoutSettingsServiceAbstraction, + KdfConfigService, + ], + }), + safeProvider({ + provide: PasswordLoginStrategyData, + useClass: PasswordLoginStrategyData, + deps: [], + }), ]; @NgModule({ diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index aeee1fa104c..4f7d4b6b600 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -16,6 +16,7 @@ import { import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -164,9 +165,10 @@ export class AddEditComponent implements OnInit, OnDestroy { } }); - this.policyService - .getAll$(PolicyType.SendOptions) + this.accountService.activeAccount$ .pipe( + getUserId, + switchMap((userId) => this.policyService.getAll$(PolicyType.SendOptions, userId)), map((policies) => policies?.some((p) => p.data.disableHideEmail)), takeUntil(this.destroy$), ) diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index b86d48d3911..26f645d89ef 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -7,10 +7,7 @@ import { concatMap, firstValueFrom, map, Observable, Subject, takeUntil } from " import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { - isMember, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -235,9 +232,12 @@ export class AddEditComponent implements OnInit, OnDestroy { this.ownershipOptions.push({ name: myEmail, value: null }); } - const orgs = await this.organizationService.getAll(); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); orgs - .filter(isMember) + .filter((org) => org.isMember) .sort(Utils.getSortFunction(this.i18nService, "name")) .forEach((o) => { if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { @@ -313,10 +313,14 @@ export class AddEditComponent implements OnInit, OnDestroy { } // Only Admins can clone a cipher to different owner if (this.cloneMode && this.cipher.organizationId != null) { - const cipherOrg = (await firstValueFrom(this.organizationService.memberOrganizations$)).find( - (o) => o.id === this.cipher.organizationId, + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + const cipherOrg = ( + await firstValueFrom(this.organizationService.memberOrganizations$(activeUserId)) + ).find((o) => o.id === this.cipher.organizationId); + if (cipherOrg != null && !cipherOrg.isAdmin && !cipherOrg.permissions.editAnyCollection) { this.ownershipOptions = [{ name: cipherOrg.name, value: cipherOrg.id }]; } @@ -658,7 +662,13 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.collections.length === 1) { (this.collections[0] as any).checked = true; } - const org = await this.organizationService.get(this.cipher.organizationId); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const org = ( + await firstValueFrom(this.organizationService.organizations$(activeUserId)) + ).find((org) => org.id === this.cipher.organizationId); if (org != null) { this.cipher.organizationUseTotp = org.useTotp; } diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index a3b635f151d..9f1dd31da0c 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -6,8 +6,8 @@ import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -16,6 +16,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -26,7 +27,7 @@ import { KeyService } from "@bitwarden/key-management"; export class AttachmentsComponent implements OnInit { @Input() cipherId: string; @Input() viewOnly: boolean; - @Output() onUploadedAttachment = new EventEmitter(); + @Output() onUploadedAttachment = new EventEmitter(); @Output() onDeletedAttachment = new EventEmitter(); @Output() onReuploadedAttachment = new EventEmitter(); @@ -34,7 +35,7 @@ export class AttachmentsComponent implements OnInit { cipherDomain: Cipher; canAccessAttachments: boolean; formPromise: Promise; - deletePromises: { [id: string]: Promise } = {}; + deletePromises: { [id: string]: Promise } = {}; reuploadPromises: { [id: string]: Promise } = {}; emergencyAccessId?: string = null; protected componentName = ""; @@ -96,7 +97,7 @@ export class AttachmentsComponent implements OnInit { title: null, message: this.i18nService.t("attachmentSaved"), }); - this.onUploadedAttachment.emit(); + this.onUploadedAttachment.emit(this.cipher); } catch (e) { this.logService.error(e); } @@ -125,7 +126,16 @@ export class AttachmentsComponent implements OnInit { try { this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); - await this.deletePromises[attachment.id]; + const updatedCipher = await this.deletePromises[attachment.id]; + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const cipher = new Cipher(updatedCipher); + this.cipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + this.toastService.showToast({ variant: "success", title: null, @@ -140,7 +150,7 @@ export class AttachmentsComponent implements OnInit { } this.deletePromises[attachment.id] = null; - this.onDeletedAttachment.emit(); + this.onDeletedAttachment.emit(this.cipher); } async download(attachment: AttachmentView) { diff --git a/libs/angular/src/vault/components/icon.component.html b/libs/angular/src/vault/components/icon.component.html index f16545617c9..caca9ded04f 100644 --- a/libs/angular/src/vault/components/icon.component.html +++ b/libs/angular/src/vault/components/icon.component.html @@ -4,7 +4,7 @@ [src]="data.image" [appFallbackSrc]="data.fallbackImage" *ngIf="data.imageEnabled && data.image" - class="tw-h-6 tw-w-6 tw-rounded-md" + class="tw-size-6 tw-rounded-md" alt="" decoding="async" loading="lazy" diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 4ef00e90063..f093aeb1330 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -1,7 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { BehaviorSubject, firstValueFrom, from, Subject, switchMap, takeUntil } from "rxjs"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -40,7 +41,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy { constructor( protected searchService: SearchService, protected cipherService: CipherService, - ) {} + ) { + this.cipherService.cipherViews$.pipe(takeUntilDestroyed()).subscribe((ciphers) => { + void this.doSearch(ciphers); + this.loaded = true; + }); + } ngOnInit(): void { this._searchText$ @@ -117,7 +123,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy { protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; protected async doSearch(indexedCiphers?: CipherView[]) { - indexedCiphers = indexedCiphers ?? (await this.cipherService.getAllDecrypted()); + indexedCiphers = indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$)); const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$); if (failedCiphers != null && failedCiphers.length > 0) { diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 18caa875e03..227bc14f1b1 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -11,7 +11,7 @@ import { OnInit, Output, } from "@angular/core"; -import { firstValueFrom, map, Observable } from "rxjs"; +import { filter, firstValueFrom, map, Observable } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -20,9 +20,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -144,11 +144,15 @@ export class ViewComponent implements OnDestroy, OnInit { async load() { this.cleanUp(); - const cipher = await this.cipherService.get(this.cipherId); const activeUserId = await firstValueFrom(this.activeUserId$); - this.cipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + // Grab individual cipher from `cipherViews$` for the most up-to-date information + this.cipher = await firstValueFrom( + this.cipherService.cipherViews$.pipe( + map((ciphers) => ciphers.find((c) => c.id === this.cipherId)), + filter((cipher) => !!cipher), + ), ); + this.canAccessPremium = await firstValueFrom( this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), ); diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts index ba19cf808ee..960590dab53 100644 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts @@ -8,10 +8,8 @@ import { Account, AccountService } from "@bitwarden/common/auth/abstractions/acc import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { NewDeviceVerificationNoticeService } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; import { VaultProfileService } from "../services/vault-profile.service"; import { NewDeviceVerificationNoticeGuard } from "./new-device-verification-notice.guard"; @@ -36,7 +34,7 @@ describe("NewDeviceVerificationNoticeGuard", () => { return Promise.resolve(false); }); - const isSelfHost = jest.fn().mockResolvedValue(false); + const isSelfHost = jest.fn().mockReturnValue(false); const getProfileTwoFactorEnabled = jest.fn().mockResolvedValue(false); const policyAppliesToActiveUser$ = jest.fn().mockReturnValue(new BehaviorSubject(false)); const noticeState$ = jest.fn().mockReturnValue(new BehaviorSubject(null)); @@ -139,6 +137,12 @@ describe("NewDeviceVerificationNoticeGuard", () => { expect(await newDeviceGuard()).toBe(true); }); + it("returns `true` when the profile service throws an error", async () => { + getProfileCreationDate.mockRejectedValueOnce(new Error("test")); + + expect(await newDeviceGuard()).toBe(true); + }); + describe("temp flag", () => { beforeEach(() => { getFeatureFlag.mockImplementation((key) => { diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts index 8b406877a12..09d6b3313c4 100644 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts @@ -1,6 +1,6 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, Router } from "@angular/router"; -import { Observable, firstValueFrom } from "rxjs"; +import { firstValueFrom, Observable } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -8,10 +8,8 @@ import { Account, AccountService } from "@bitwarden/common/auth/abstractions/acc import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { NewDeviceVerificationNoticeService } from "@bitwarden/vault"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; import { VaultProfileService } from "../services/vault-profile.service"; export const NewDeviceVerificationNoticeGuard: CanActivateFn = async ( @@ -47,17 +45,23 @@ export const NewDeviceVerificationNoticeGuard: CanActivateFn = async ( return router.createUrlTree(["/login"]); } - const has2FAEnabled = await hasATwoFactorProviderEnabled(vaultProfileService, currentAcct.id); - const isSelfHosted = await platformUtilsService.isSelfHost(); - const requiresSSO = await isSSORequired(policyService); - const isProfileLessThanWeekOld = await profileIsLessThanWeekOld( - vaultProfileService, - currentAcct.id, - ); + try { + const isSelfHosted = platformUtilsService.isSelfHost(); + const requiresSSO = await isSSORequired(policyService); + const has2FAEnabled = await hasATwoFactorProviderEnabled(vaultProfileService, currentAcct.id); + const isProfileLessThanWeekOld = await profileIsLessThanWeekOld( + vaultProfileService, + currentAcct.id, + ); - // When any of the following are true, the device verification notice is - // not applicable for the user. - if (has2FAEnabled || isSelfHosted || requiresSSO || isProfileLessThanWeekOld) { + // When any of the following are true, the device verification notice is + // not applicable for the user. + if (has2FAEnabled || isSelfHosted || requiresSSO || isProfileLessThanWeekOld) { + return true; + } + } catch { + // Skip showing the notice if there was a problem determining applicability + // The most likely problem to occur is the user not having a network connection return true; } diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index 260780e1964..aab06a69add 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -4,28 +4,23 @@ import { Injectable } from "@angular/core"; import { firstValueFrom, from, map, mergeMap, Observable, switchMap } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; -import { - isMember, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; +import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state"; import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "../../abstractions/deprecated-vault-filter.service"; import { DynamicTreeNode } from "../models/dynamic-tree-node.model"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { COLLAPSED_GROUPINGS } from "./../../../../../common/src/vault/services/key-state/collapsed-groupings.state"; - const NestingDelimiter = "/"; @Injectable() @@ -56,9 +51,12 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti } async buildOrganizations(): Promise { - let organizations = await this.organizationService.getAll(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + let organizations = await firstValueFrom(this.organizationService.organizations$(userId)); if (organizations != null) { - organizations = organizations.filter(isMember).sort((a, b) => a.name.localeCompare(b.name)); + organizations = organizations + .filter((o) => o.isMember) + .sort((a, b) => a.name.localeCompare(b.name)); } return organizations; diff --git a/libs/angular/tsconfig.json b/libs/angular/tsconfig.json index b638410a6a8..c603e5cf170 100644 --- a/libs/angular/tsconfig.json +++ b/libs/angular/tsconfig.json @@ -13,7 +13,6 @@ "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], - "@bitwarden/importer/core": ["../importer/src"], "@bitwarden/key-management": ["../key-management/src"], "@bitwarden/platform": ["../platform/src"], "@bitwarden/ui-common": ["../ui/common/src"], diff --git a/libs/auth/src/angular/icons/device-verification.icon.ts b/libs/auth/src/angular/icons/device-verification.icon.ts new file mode 100644 index 00000000000..b1be4efdfb3 --- /dev/null +++ b/libs/auth/src/angular/icons/device-verification.icon.ts @@ -0,0 +1,18 @@ +import { svgIcon } from "@bitwarden/components"; + +export const DeviceVerificationIcon = svgIcon` + + + + + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 0e86ee7fc8e..0ec92d54547 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -12,3 +12,4 @@ export * from "./registration-lock-alt.icon"; export * from "./registration-expired-link.icon"; export * from "./sso-key.icon"; export * from "./two-factor-timeout.icon"; +export * from "./device-verification.icon"; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index 66111f3e5af..67ab68852b2 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -71,3 +71,6 @@ export * from "./self-hosted-env-config-dialog/self-hosted-env-config-dialog.com // login approval export * from "./login-approval/login-approval.component"; export * from "./login-approval/default-login-approval-component.service"; + +// device verification +export * from "./new-device-verification/new-device-verification.component"; diff --git a/libs/auth/src/angular/login-approval/login-approval.component.html b/libs/auth/src/angular/login-approval/login-approval.component.html index c0cb9b9caf4..2115bdbff11 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.html +++ b/libs/auth/src/angular/login-approval/login-approval.component.html @@ -1,5 +1,5 @@ - {{ "areYouTryingtoLogin" | i18n }} + {{ "areYouTryingToAccessYourAccount" | i18n }}
@@ -8,7 +8,7 @@ -

{{ "logInAttemptBy" | i18n: email }}

+

{{ "accessAttemptBy" | i18n: email }}

{{ "fingerprintPhraseHeader" | i18n }}

{{ fingerprintPhrase }}

@@ -35,7 +35,7 @@ [bitAction]="approveLogin" [disabled]="loading" > - {{ "confirmLogIn" | i18n }} + {{ "confirmAccess" | i18n }} diff --git a/libs/auth/src/angular/login-approval/login-approval.component.ts b/libs/auth/src/angular/login-approval/login-approval.component.ts index 3b44f545abb..54d90306e5c 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.ts +++ b/libs/auth/src/angular/login-approval/login-approval.component.ts @@ -85,7 +85,7 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { } const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey); - this.email = await await firstValueFrom( + this.email = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.email)), ); this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase( diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index a3f5e062e4f..4c93c79d6fe 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -202,7 +202,7 @@ export class LoginDecryptionOptionsComponent implements OnInit { }); const autoEnrollStatus$ = defer(() => - this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(), + this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId), ).pipe( switchMap((organizationIdentifier) => { if (organizationIdentifier == undefined) { diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html index a1d0f200c15..ba26ba77cb0 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html @@ -1,6 +1,20 @@
-

{{ "makeSureYourAccountIsUnlockedAndTheFingerprintEtc" | i18n }}

+

+ {{ "notificationSentDevicePart1" | i18n }} + {{ "notificationSentDeviceAnchor" | i18n }}. {{ "notificationSentDevicePart2" | i18n }} +

+

+ {{ "notificationSentDeviceComplete" | i18n }} +

{{ "fingerprintPhraseHeader" | i18n }}
{{ fingerprintPhrase }} diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index b9a5ee4fe73..00e2d621c47 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -29,6 +29,7 @@ import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -71,6 +72,8 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { protected showResendNotification = false; protected Flow = Flow; protected flow = Flow.StandardAuthRequest; + protected webVaultUrl: string; + protected deviceManagementUrl: string; constructor( private accountService: AccountService, @@ -81,6 +84,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private authService: AuthService, private cryptoFunctionService: CryptoFunctionService, private deviceTrustService: DeviceTrustServiceAbstraction, + private environmentService: EnvironmentService, private i18nService: I18nService, private logService: LogService, private loginEmailService: LoginEmailServiceAbstraction, @@ -109,6 +113,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { this.logService.error("Failed to use approved auth request: " + e.message); }); }); + + // Get the web vault URL from the environment service + this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { + this.webVaultUrl = env.getWebVaultUrl(); + this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`; + }); } async ngOnInit(): Promise { diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index f9aaa5d1e05..66fe2503508 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -275,6 +275,12 @@ export class LoginComponent implements OnInit, OnDestroy { return; } + // Redirect to device verification if this is an unknown device + if (authResult.requiresDeviceVerification) { + await this.router.navigate(["device-verification"]); + return; + } + await this.loginSuccessHandlerService.run(authResult.userId); if (authResult.forcePasswordReset != ForceSetPasswordReason.None) { diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.html b/libs/auth/src/angular/new-device-verification/new-device-verification.component.html new file mode 100644 index 00000000000..2f807d32993 --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.html @@ -0,0 +1,36 @@ +
+ + {{ "verificationCode" | i18n }} + + + + + +
+ +
+
diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts new file mode 100644 index 00000000000..6e0f9eec05e --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts @@ -0,0 +1,163 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { + AsyncActionsModule, + ButtonModule, + FormFieldModule, + IconButtonModule, + LinkModule, + ToastService, +} from "@bitwarden/components"; + +import { LoginEmailServiceAbstraction } from "../../common/abstractions/login-email.service"; +import { LoginStrategyServiceAbstraction } from "../../common/abstractions/login-strategy.service"; +import { PasswordLoginStrategy } from "../../common/login-strategies/password-login.strategy"; + +/** + * Component for verifying a new device via a one-time password (OTP). + */ +@Component({ + standalone: true, + selector: "app-new-device-verification", + templateUrl: "./new-device-verification.component.html", + imports: [ + CommonModule, + ReactiveFormsModule, + AsyncActionsModule, + JslibModule, + ButtonModule, + FormFieldModule, + IconButtonModule, + LinkModule, + ], +}) +export class NewDeviceVerificationComponent implements OnInit, OnDestroy { + formGroup = this.formBuilder.group({ + code: [ + "", + { + validators: [Validators.required], + updateOn: "change", + }, + ], + }); + + protected disableRequestOTP = false; + private destroy$ = new Subject(); + protected authenticationSessionTimeoutRoute = "/authentication-timeout"; + + constructor( + private router: Router, + private formBuilder: FormBuilder, + private passwordLoginStrategy: PasswordLoginStrategy, + private apiService: ApiService, + private loginStrategyService: LoginStrategyServiceAbstraction, + private logService: LogService, + private toastService: ToastService, + private i18nService: I18nService, + private syncService: SyncService, + private loginEmailService: LoginEmailServiceAbstraction, + ) {} + + async ngOnInit() { + // Redirect to timeout route if session expires + this.loginStrategyService.authenticationSessionTimeout$ + .pipe(takeUntil(this.destroy$)) + .subscribe((expired) => { + if (!expired) { + return; + } + + try { + void this.router.navigate([this.authenticationSessionTimeoutRoute]); + } catch (err) { + this.logService.error( + `Failed to navigate to ${this.authenticationSessionTimeoutRoute} route`, + err, + ); + } + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + /** + * Resends the OTP for device verification. + */ + async resendOTP() { + this.disableRequestOTP = true; + try { + const email = await this.loginStrategyService.getEmail(); + const masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash(); + + if (!email || !masterPasswordHash) { + throw new Error("Missing email or master password hash"); + } + + await this.apiService.send( + "POST", + "/accounts/resend-new-device-otp", + { + email: email, + masterPasswordHash: masterPasswordHash, + }, + false, + false, + ); + } catch (e) { + this.logService.error(e); + } finally { + this.disableRequestOTP = false; + } + } + + /** + * Submits the OTP for device verification. + */ + submit = async (): Promise => { + const codeControl = this.formGroup.get("code"); + if (!codeControl || !codeControl.value) { + return; + } + + try { + const authResult = await this.loginStrategyService.logInNewDeviceVerification( + codeControl.value, + ); + + if (authResult.requiresTwoFactor) { + await this.router.navigate(["/2fa"]); + return; + } + + if (authResult.forcePasswordReset) { + await this.router.navigate(["/update-temp-password"]); + return; + } + + this.loginEmailService.clearValues(); + + await this.syncService.fullSync(true); + + // If verification succeeds, navigate to vault + await this.router.navigate(["/vault"]); + } catch (e) { + this.logService.error(e); + const errorMessage = + (e as any)?.response?.error_description ?? this.i18nService.t("errorOccurred"); + codeControl.setErrors({ serverError: { message: errorMessage } }); + } + }; +} diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index 31b3f7db92a..c419e1f427f 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -16,7 +16,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; -import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "../../../common"; +import { + LoginStrategyServiceAbstraction, + LoginSuccessHandlerService, + PasswordLoginCredentials, +} from "../../../common"; import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; import { InputPasswordComponent } from "../../input-password/input-password.component"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -68,6 +72,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { private loginStrategyService: LoginStrategyServiceAbstraction, private logService: LogService, private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + private loginSuccessHandlerService: LoginSuccessHandlerService, ) {} async ngOnInit() { @@ -189,6 +194,8 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { message: this.i18nService.t("youHaveBeenLoggedIn"), }); + await this.loginSuccessHandlerService.run(authenticationResult.userId); + await this.router.navigate(["/vault"]); } catch (e) { // If login errors, redirect to login page per product. Don't show error diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts index 7a2b334eb42..726110663fc 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts @@ -11,8 +11,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts index 84c580662be..6c9ce8f9267 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts @@ -12,8 +12,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index 4583332cb88..b4373bfe96e 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -36,6 +36,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; import { AsyncActionsModule, ButtonModule, @@ -89,6 +90,7 @@ export class SsoComponent implements OnInit { protected state: string | undefined; protected codeChallenge: string | undefined; protected clientId: SsoClientType | undefined; + protected activeUserId: UserId | undefined; formPromise: Promise | undefined; initiateSsoFormPromise: Promise | undefined; @@ -130,6 +132,8 @@ export class SsoComponent implements OnInit { } async ngOnInit() { + this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const qParams: QueryParams = await firstValueFrom(this.route.queryParams); // This if statement will pass on the second portion of the SSO flow @@ -384,7 +388,10 @@ export class SsoComponent implements OnInit { // - TDE login decryption options component // - Browser SSO on extension open // Note: you cannot set this in state before 2FA b/c there won't be an account in state. - await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier); + await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier( + orgSsoIdentifier, + this.activeUserId, + ); // Users enrolled in admin acct recovery can be forced to set a new password after // having the admin set a temp password for them (affects TDE & standard users) diff --git a/libs/auth/src/angular/user-verification/user-verification-form-input.component.html b/libs/auth/src/angular/user-verification/user-verification-form-input.component.html index f532a3b23fd..56bce040d2f 100644 --- a/libs/auth/src/angular/user-verification/user-verification-form-input.component.html +++ b/libs/auth/src/angular/user-verification/user-verification-form-input.component.html @@ -120,7 +120,7 @@
{{ "enterVerificationCodeSentToEmail" | i18n }} -

+

diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index 1088d6de736..bd725f29024 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -47,7 +47,6 @@ export abstract class LoginStrategyServiceAbstraction { * Auth Request. Otherwise, it will return null. */ getAuthRequestId: () => Promise; - /** * Sends a token request to the server using the provided credentials. */ @@ -74,7 +73,11 @@ export abstract class LoginStrategyServiceAbstraction { */ makePreloginKey: (masterPassword: string, email: string) => Promise; /** - * Emits true if the two factor session has expired. + * Emits true if the authentication session has expired. */ - twoFactorTimeout$: Observable; + authenticationSessionTimeout$: Observable; + /** + * Sends a token request to the server with the provided device verification OTP. + */ + logInNewDeviceVerification: (deviceVerificationOtp: string) => Promise; } diff --git a/libs/auth/src/common/index.ts b/libs/auth/src/common/index.ts index 43efd7c6387..97909bdc449 100644 --- a/libs/auth/src/common/index.ts +++ b/libs/auth/src/common/index.ts @@ -6,3 +6,4 @@ export * from "./models"; export * from "./types"; export * from "./services"; export * from "./utilities"; +export * from "./login-strategies"; diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index cec4481cd8d..7c56e2a58c8 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -9,8 +9,8 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; diff --git a/libs/auth/src/common/login-strategies/index.ts b/libs/auth/src/common/login-strategies/index.ts new file mode 100644 index 00000000000..166ef935e08 --- /dev/null +++ b/libs/auth/src/common/login-strategies/index.ts @@ -0,0 +1 @@ +export { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy"; diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 50443bab0ea..fbd6b79f19d 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -4,6 +4,7 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -12,6 +13,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; @@ -19,8 +21,8 @@ import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/mod import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -76,8 +78,8 @@ const twoFactorToken = "TWO_FACTOR_TOKEN"; const twoFactorRemember = true; export function identityTokenResponseFactory( - masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null, - userDecryptionOptions: IUserDecryptionOptionsServerResponse = null, + masterPasswordPolicyResponse: MasterPasswordPolicyResponse | undefined = undefined, + userDecryptionOptions: IUserDecryptionOptionsServerResponse | undefined = undefined, ) { return new IdentityTokenResponse({ ForcePasswordReset: false, @@ -155,7 +157,7 @@ describe("LoginStrategy", () => { passwordStrengthService, policyService, loginStrategyService, - accountService, + accountService as unknown as AccountService, masterPasswordService, keyService, encryptService, @@ -286,13 +288,16 @@ describe("LoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); - expect(result).toEqual({ - userId: userId, - forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset, - resetMasterPassword: true, - twoFactorProviders: null, - captchaSiteKey: "", - } as AuthResult); + const expected = new AuthResult(); + expected.userId = userId; + expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset; + expected.resetMasterPassword = true; + expected.twoFactorProviders = {} as Partial< + Record> + >; + expected.captchaSiteKey = ""; + expected.twoFactorProviders = null; + expect(result).toEqual(expected); }); it("rejects login if CAPTCHA is required", async () => { @@ -377,10 +382,11 @@ describe("LoginStrategy", () => { expect(tokenService.clearTwoFactorToken).toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = { 0: null } as Record< - TwoFactorProviderType, - Record + expected.twoFactorProviders = { 0: null } as unknown as Partial< + Record> >; + expected.email = ""; + expected.ssoEmail2FaSessionToken = undefined; expect(result).toEqual(expected); }); @@ -460,14 +466,19 @@ describe("LoginStrategy", () => { it("sends 2FA token provided by user to server (two-step)", async () => { // Simulate a partially completed login cache = new PasswordLoginStrategyData(); - cache.tokenRequest = new PasswordTokenRequest(email, masterPasswordHash, null, null); + cache.tokenRequest = new PasswordTokenRequest( + email, + masterPasswordHash, + "", + new TokenTwoFactorRequest(), + ); passwordLoginStrategy = new PasswordLoginStrategy( cache, passwordStrengthService, policyService, loginStrategyService, - accountService, + accountService as AccountService, masterPasswordService, keyService, encryptService, @@ -489,7 +500,7 @@ describe("LoginStrategy", () => { await passwordLoginStrategy.logInTwoFactor( new TokenTwoFactorRequest(twoFactorProviderType, twoFactorToken, twoFactorRemember), - null, + "", ); expect(apiService.postIdentityToken).toHaveBeenCalledWith( @@ -503,4 +514,54 @@ describe("LoginStrategy", () => { ); }); }); + + describe("Device verification", () => { + it("processes device verification response", async () => { + const captchaToken = "test-captcha-token"; + const deviceVerificationResponse = new IdentityDeviceVerificationResponse({ + error: "invalid_grant", + error_description: "Device verification required.", + email: "test@bitwarden.com", + deviceVerificationRequest: true, + captchaToken: captchaToken, + }); + + apiService.postIdentityToken.mockResolvedValue(deviceVerificationResponse); + + cache = new PasswordLoginStrategyData(); + cache.tokenRequest = new PasswordTokenRequest( + email, + masterPasswordHash, + "", + new TokenTwoFactorRequest(), + ); + + passwordLoginStrategy = new PasswordLoginStrategy( + cache, + passwordStrengthService, + policyService, + loginStrategyService, + accountService as AccountService, + masterPasswordService, + keyService, + encryptService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + twoFactorService, + userDecryptionOptionsService, + billingAccountProfileStateService, + vaultTimeoutSettingsService, + kdfConfigService, + ); + + const result = await passwordLoginStrategy.logIn(credentials); + + expect(result.requiresDeviceVerification).toBe(true); + }); + }); }); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 25f99f47840..b4eef1a0276 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,6 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs"; +import { BehaviorSubject, filter, firstValueFrom, timeout, Observable } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -18,14 +16,15 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request"; import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -51,14 +50,19 @@ import { import { UserDecryptionOptions } from "../models/domain/user-decryption-options"; import { CacheData } from "../services/login-strategies/login-strategy.state"; -type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse; +type IdentityResponse = + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse; export abstract class LoginStrategyData { tokenRequest: | UserApiTokenRequest | PasswordTokenRequest | SsoTokenRequest - | WebAuthnLoginTokenRequest; + | WebAuthnLoginTokenRequest + | undefined; captchaBypassToken?: string; /** User's entered email obtained pre-login. */ @@ -67,6 +71,8 @@ export abstract class LoginStrategyData { export abstract class LoginStrategy { protected abstract cache: BehaviorSubject; + protected sessionTimeoutSubject = new BehaviorSubject(false); + sessionTimeout$: Observable = this.sessionTimeoutSubject.asObservable(); constructor( protected accountService: AccountService, @@ -100,9 +106,12 @@ export abstract class LoginStrategy { async logInTwoFactor( twoFactor: TokenTwoFactorRequest, - captchaResponse: string = null, + captchaResponse: string | null = null, ): Promise { const data = this.cache.value; + if (!data.tokenRequest) { + throw new Error("Token request is undefined"); + } data.tokenRequest.setTwoFactor(twoFactor); this.cache.next(data); const [authResult] = await this.startLogIn(); @@ -113,6 +122,9 @@ export abstract class LoginStrategy { await this.twoFactorService.clearSelectedProvider(); const tokenRequest = this.cache.value.tokenRequest; + if (!tokenRequest) { + throw new Error("Token request is undefined"); + } const response = await this.apiService.postIdentityToken(tokenRequest); if (response instanceof IdentityTwoFactorResponse) { @@ -121,6 +133,8 @@ export abstract class LoginStrategy { return [await this.processCaptchaResponse(response), response]; } else if (response instanceof IdentityTokenResponse) { return [await this.processTokenResponse(response), response]; + } else if (response instanceof IdentityDeviceVerificationResponse) { + return [await this.processDeviceVerificationResponse(response), response]; } throw new Error("Invalid response object."); @@ -176,8 +190,8 @@ export abstract class LoginStrategy { await this.accountService.addAccount(userId, { name: accountInformation.name, - email: accountInformation.email, - emailVerified: accountInformation.email_verified, + email: accountInformation.email ?? "", + emailVerified: accountInformation.email_verified ?? false, }); await this.accountService.switchAccount(userId); @@ -230,7 +244,7 @@ export abstract class LoginStrategy { ); await this.billingAccountProfileStateService.setHasPremium( - accountInformation.premium, + accountInformation.premium ?? false, false, userId, ); @@ -291,6 +305,9 @@ export abstract class LoginStrategy { try { const userKey = await this.keyService.getUserKeyWithLegacySupport(userId); const [publicKey, privateKey] = await this.keyService.makeKeyPair(userKey); + if (!privateKey.encryptedString) { + throw new Error("Failed to create encrypted private key"); + } await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString)); return privateKey.encryptedString; } catch (e) { @@ -316,7 +333,8 @@ export abstract class LoginStrategy { await this.twoFactorService.setProviders(response); this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken; - result.email = response.email; + + result.email = response.email ?? ""; return result; } @@ -355,4 +373,22 @@ export abstract class LoginStrategy { ), ); } + + /** + * Handles the response from the server when a device verification is required. + * It sets the requiresDeviceVerification flag to true and caches the captcha token if it came back. + * + * @param {IdentityDeviceVerificationResponse} response - The response from the server indicating that device verification is required. + * @returns {Promise} - A promise that resolves to an AuthResult object + */ + protected async processDeviceVerificationResponse( + response: IdentityDeviceVerificationResponse, + ): Promise { + const result = new AuthResult(); + result.requiresDeviceVerification = true; + + // Extend cached data with captcha bypass token if it came back. + this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); + return result; + } } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 4ee4fcaeb38..1b0613d4da3 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -13,8 +13,8 @@ import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/resp import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -276,4 +276,24 @@ describe("PasswordLoginStrategy", () => { ); expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); + + it("handles new device verification login with OTP", async () => { + const deviceVerificationOtp = "123456"; + const tokenResponse = identityTokenResponseFactory(); + apiService.postIdentityToken.mockResolvedValueOnce(tokenResponse); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); + + await passwordLoginStrategy.logIn(credentials); + + const result = await passwordLoginStrategy.logInNewDeviceVerification(deviceVerificationOtp); + + expect(apiService.postIdentityToken).toHaveBeenCalledWith( + expect.objectContaining({ + newDeviceOtp: deviceVerificationOtp, + }), + ); + expect(result.forcePasswordReset).toBe(ForceSetPasswordReason.None); + expect(result.resetMasterPassword).toBe(false); + expect(result.userId).toBe(userId); + }); }); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index c496b7c9674..f0a8d40f914 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -10,6 +10,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { HashPurpose } from "@bitwarden/common/platform/enums"; @@ -208,9 +209,12 @@ export class PasswordLoginStrategy extends LoginStrategy { } private getMasterPasswordPolicyOptionsFromResponse( - response: IdentityTokenResponse | IdentityTwoFactorResponse, + response: + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityDeviceVerificationResponse, ): MasterPasswordPolicyOptions { - if (response == null) { + if (response == null || response instanceof IdentityDeviceVerificationResponse) { return null; } return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy); @@ -233,4 +237,13 @@ export class PasswordLoginStrategy extends LoginStrategy { password: this.cache.value, }; } + + async logInNewDeviceVerification(deviceVerificationOtp: string): Promise { + const data = this.cache.value; + data.tokenRequest.newDeviceOtp = deviceVerificationOtp; + this.cache.next(data); + + const [authResult] = await this.startLogIn(); + return authResult; + } } diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index ec3ec43134f..96b08e98e37 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -13,9 +13,9 @@ import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/mod import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 2bb41faa0e1..dd3e7f0134d 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -8,8 +8,8 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Environment, EnvironmentService, diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 9dacce2cf00..fd8817a8c21 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -11,8 +11,8 @@ import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/maste import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index 86b2a1dd3b6..2ea6d427641 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -3,9 +3,9 @@ import { mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index 4bc0397b43a..5bc200ae1e8 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -9,9 +9,9 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 5fcbefbef2f..117e5c1f864 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -17,8 +17,8 @@ import { PreloginResponse } from "@bitwarden/common/auth/models/response/prelogi import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -321,4 +321,67 @@ describe("LoginStrategyService", () => { `PBKDF2 iterations must be at least ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1}; possible pre-login downgrade attack detected.`, ); }); + + it("returns an AuthResult on successful new device verification", async () => { + const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD"); + const deviceVerificationOtp = "123456"; + + // Setup initial login and device verification response + apiService.postPrelogin.mockResolvedValue( + new PreloginResponse({ + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + }), + ); + + apiService.postIdentityToken.mockResolvedValueOnce( + new IdentityTwoFactorResponse({ + TwoFactorProviders: ["0"], + TwoFactorProviders2: { 0: null }, + error: "invalid_grant", + error_description: "Two factor required.", + email: undefined, + ssoEmail2faSessionToken: undefined, + }), + ); + + await sut.logIn(credentials); + + // Successful device verification login + apiService.postIdentityToken.mockResolvedValueOnce( + new IdentityTokenResponse({ + ForcePasswordReset: false, + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + Key: "KEY", + PrivateKey: "PRIVATE_KEY", + ResetMasterPassword: false, + access_token: "ACCESS_TOKEN", + expires_in: 3600, + refresh_token: "REFRESH_TOKEN", + scope: "api offline_access", + token_type: "Bearer", + }), + ); + + tokenService.decodeAccessToken.calledWith("ACCESS_TOKEN").mockResolvedValue({ + sub: "USER_ID", + name: "NAME", + email: "EMAIL", + premium: false, + }); + + const result = await sut.logInNewDeviceVerification(deviceVerificationOtp); + + expect(result).toBeInstanceOf(AuthResult); + expect(apiService.postIdentityToken).toHaveBeenCalledWith( + expect.objectContaining({ + newDeviceOtp: deviceVerificationOtp, + }), + ); + }); }); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 57a653b205e..849b8e5eba1 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { combineLatestWith, distinctUntilChanged, @@ -15,6 +13,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -23,10 +22,10 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication- import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -35,9 +34,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { MasterKey } from "@bitwarden/common/types/key"; import { @@ -51,12 +47,24 @@ import { import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; -import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy"; +import { + AuthRequestLoginStrategy, + AuthRequestLoginStrategyData, +} from "../../login-strategies/auth-request-login.strategy"; import { LoginStrategy } from "../../login-strategies/login.strategy"; -import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy"; -import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy"; -import { UserApiLoginStrategy } from "../../login-strategies/user-api-login.strategy"; -import { WebAuthnLoginStrategy } from "../../login-strategies/webauthn-login.strategy"; +import { + PasswordLoginStrategy, + PasswordLoginStrategyData, +} from "../../login-strategies/password-login.strategy"; +import { SsoLoginStrategy, SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy"; +import { + UserApiLoginStrategy, + UserApiLoginStrategyData, +} from "../../login-strategies/user-api-login.strategy"; +import { + WebAuthnLoginStrategy, + WebAuthnLoginStrategyData, +} from "../../login-strategies/webauthn-login.strategy"; import { UserApiLoginCredentials, PasswordLoginCredentials, @@ -76,14 +84,15 @@ import { const sessionTimeoutLength = 5 * 60 * 1000; // 5 minutes export class LoginStrategyService implements LoginStrategyServiceAbstraction { - private sessionTimeoutSubscription: Subscription; + private sessionTimeoutSubscription: Subscription | undefined; private currentAuthnTypeState: GlobalState; private loginStrategyCacheState: GlobalState; private loginStrategyCacheExpirationState: GlobalState; - private authRequestPushNotificationState: GlobalState; - private twoFactorTimeoutSubject = new BehaviorSubject(false); + private authRequestPushNotificationState: GlobalState; + private authenticationTimeoutSubject = new BehaviorSubject(false); - twoFactorTimeout$: Observable = this.twoFactorTimeoutSubject.asObservable(); + authenticationSessionTimeout$: Observable = + this.authenticationTimeoutSubject.asObservable(); private loginStrategy$: Observable< | UserApiLoginStrategy @@ -132,7 +141,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.taskSchedulerService.registerTaskHandler( ScheduledTaskNames.loginStrategySessionTimeout, async () => { - this.twoFactorTimeoutSubject.next(true); + this.authenticationTimeoutSubject.next(true); try { await this.clearCache(); } catch (e) { @@ -153,7 +162,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getEmail(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("email$" in strategy) { + if (strategy && "email$" in strategy) { return await firstValueFrom(strategy.email$); } return null; @@ -162,7 +171,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getMasterPasswordHash(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("serverMasterKeyHash$" in strategy) { + if (strategy && "serverMasterKeyHash$" in strategy) { return await firstValueFrom(strategy.serverMasterKeyHash$); } return null; @@ -171,7 +180,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getSsoEmail2FaSessionToken(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("ssoEmail2FaSessionToken$" in strategy) { + if (strategy && "ssoEmail2FaSessionToken$" in strategy) { return await firstValueFrom(strategy.ssoEmail2FaSessionToken$); } return null; @@ -180,7 +189,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getAccessCode(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("accessCode$" in strategy) { + if (strategy && "accessCode$" in strategy) { return await firstValueFrom(strategy.accessCode$); } return null; @@ -189,7 +198,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getAuthRequestId(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("authRequestId$" in strategy) { + if (strategy && "authRequestId$" in strategy) { return await firstValueFrom(strategy.authRequestId$); } return null; @@ -204,7 +213,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { | WebAuthnLoginCredentials, ): Promise { await this.clearCache(); - this.twoFactorTimeoutSubject.next(false); + this.authenticationTimeoutSubject.next(false); await this.currentAuthnTypeState.update((_) => credentials.type); @@ -217,16 +226,19 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { // If the popup uses its own instance of this service, this can be removed. const ownedCredentials = { ...credentials }; - const result = await strategy.logIn(ownedCredentials as any); + const result = await strategy?.logIn(ownedCredentials as any); - if (result != null && !result.requiresTwoFactor) { + if (result != null && !result.requiresTwoFactor && !result.requiresDeviceVerification) { await this.clearCache(); } else { - // Cache the strategy data so we can attempt again later with 2fa. Cache supports different contexts - await this.loginStrategyCacheState.update((_) => strategy.exportCache()); + // Cache the strategy data so we can attempt again later with 2fa or device verification + await this.loginStrategyCacheState.update((_) => strategy?.exportCache() ?? null); await this.startSessionTimeout(); } + if (!result) { + throw new Error("No auth result returned"); + } return result; } @@ -260,9 +272,46 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { } } + /** + * Sends a token request to the server with the provided device verification OTP. + * Returns an error if no session data is found or if the current login strategy does not support device verification. + * @param deviceVerificationOtp The OTP to send to the server for device verification. + * @returns The result of the token request. + */ + async logInNewDeviceVerification(deviceVerificationOtp: string): Promise { + if (!(await this.isSessionValid())) { + throw new Error(this.i18nService.t("sessionTimeout")); + } + + const strategy = await firstValueFrom(this.loginStrategy$); + if (strategy == null) { + throw new Error("No login strategy found."); + } + + if (!("logInNewDeviceVerification" in strategy)) { + throw new Error("Current login strategy does not support device verification."); + } + + try { + const result = await strategy.logInNewDeviceVerification(deviceVerificationOtp); + + // Only clear cache if device verification succeeds + if (result !== null && !result.requiresDeviceVerification) { + await this.clearCache(); + } + return result; + } catch (e) { + // Clear the cache if there is an unhandled client-side error + if (!(e instanceof ErrorResponse)) { + await this.clearCache(); + } + throw e; + } + } + async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); - let kdfConfig: KdfConfig = null; + let kdfConfig: KdfConfig | undefined; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { @@ -275,12 +324,15 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { preloginResponse.kdfParallelism, ); } - } catch (e) { + } catch (e: any) { if (e == null || e.statusCode !== 404) { throw e; } } + if (!kdfConfig) { + throw new Error("KDF config is required"); + } kdfConfig.validateKdfConfigForPrelogin(); return await this.keyService.makeMasterKey(masterPassword, email, kdfConfig); @@ -289,7 +341,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private async clearCache(): Promise { await this.currentAuthnTypeState.update((_) => null); await this.loginStrategyCacheState.update((_) => null); - this.twoFactorTimeoutSubject.next(false); + this.authenticationTimeoutSubject.next(false); await this.clearSessionTimeout(); } @@ -360,7 +412,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { switch (strategy) { case AuthenticationType.Password: return new PasswordLoginStrategy( - data?.password, + data?.password ?? new PasswordLoginStrategyData(), this.passwordStrengthService, this.policyService, this, @@ -368,7 +420,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); case AuthenticationType.Sso: return new SsoLoginStrategy( - data?.sso, + data?.sso ?? new SsoLoginStrategyData(), this.keyConnectorService, this.deviceTrustService, this.authRequestService, @@ -377,19 +429,22 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( - data?.userApiKey, + data?.userApiKey ?? new UserApiLoginStrategyData(), this.environmentService, this.keyConnectorService, ...sharedDeps, ); case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( - data?.authRequest, + data?.authRequest ?? new AuthRequestLoginStrategyData(), this.deviceTrustService, ...sharedDeps, ); case AuthenticationType.WebAuthn: - return new WebAuthnLoginStrategy(data?.webAuthn, ...sharedDeps); + return new WebAuthnLoginStrategy( + data?.webAuthn ?? new WebAuthnLoginStrategyData(), + ...sharedDeps, + ); } }), ); diff --git a/libs/auth/src/common/services/pin/pin.service.implementation.ts b/libs/auth/src/common/services/pin/pin.service.implementation.ts index 01fc77e4a03..9b86440b364 100644 --- a/libs/auth/src/common/services/pin/pin.service.implementation.ts +++ b/libs/auth/src/common/services/pin/pin.service.implementation.ts @@ -4,8 +4,8 @@ import { firstValueFrom, map } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; diff --git a/libs/auth/src/common/services/pin/pin.service.spec.ts b/libs/auth/src/common/services/pin/pin.service.spec.ts index d254be4e875..1d6443535bc 100644 --- a/libs/auth/src/common/services/pin/pin.service.spec.ts +++ b/libs/auth/src/common/services/pin/pin.service.spec.ts @@ -1,8 +1,8 @@ import { mock } from "jest-mock-extended"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; diff --git a/libs/common/package.json b/libs/common/package.json index 5e0f5ae20c6..ad2771e2fff 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -15,6 +15,7 @@ "scripts": { "clean": "rimraf dist", "build": "npm run clean && tsc", - "build:watch": "npm run clean && tsc -watch" + "build:watch": "npm run clean && tsc -watch", + "test": "jest" } } diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index 05e44d5db18..d45448ce698 100644 --- a/libs/common/spec/fake-account-service.ts +++ b/libs/common/spec/fake-account-service.ts @@ -35,6 +35,8 @@ export class FakeAccountService implements AccountService { activeAccountSubject = new ReplaySubject(1); // eslint-disable-next-line rxjs/no-exposed-subjects -- test class accountActivitySubject = new ReplaySubject>(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + accountVerifyDevicesSubject = new ReplaySubject(1); private _activeUserId: UserId; get activeUserId() { return this._activeUserId; @@ -42,6 +44,7 @@ export class FakeAccountService implements AccountService { accounts$ = this.accountsSubject.asObservable(); activeAccount$ = this.activeAccountSubject.asObservable(); accountActivity$ = this.accountActivitySubject.asObservable(); + accountVerifyNewDeviceLogin$ = this.accountVerifyDevicesSubject.asObservable(); get sortedUserIds$() { return this.accountActivity$.pipe( map((activity) => { @@ -67,6 +70,11 @@ export class FakeAccountService implements AccountService { this.activeAccountSubject.next(null); this.accountActivitySubject.next(accountActivity); } + + setAccountVerifyNewDeviceLogin(userId: UserId, verifyNewDeviceLogin: boolean): Promise { + return this.mock.setAccountVerifyNewDeviceLogin(userId, verifyNewDeviceLogin); + } + setAccountActivity(userId: UserId, lastActivity: Date): Promise { this.accountActivitySubject.next({ ...this.accountActivitySubject["_buffer"][0], diff --git a/libs/common/spec/matrix.spec.ts b/libs/common/spec/matrix.spec.ts new file mode 100644 index 00000000000..b1a5e7a9644 --- /dev/null +++ b/libs/common/spec/matrix.spec.ts @@ -0,0 +1,76 @@ +import { Matrix } from "./matrix"; + +class TestObject { + value: number = 0; + + constructor() {} + + increment() { + this.value++; + } +} + +describe("matrix", () => { + it("caches entries in a matrix properly with a single argument", () => { + const mockFunction = jest.fn(); + const getter = Matrix.autoMockMethod(mockFunction, () => new TestObject()); + + const obj = getter("test1"); + expect(obj.value).toBe(0); + + // Change the state of the object + obj.increment(); + + // Should return the same instance the second time this is called + expect(getter("test1").value).toBe(1); + + // Using the getter should not call the mock function + expect(mockFunction).not.toHaveBeenCalled(); + + const mockedFunctionReturn1 = mockFunction("test1"); + expect(mockedFunctionReturn1.value).toBe(1); + + // Totally new value + const mockedFunctionReturn2 = mockFunction("test2"); + expect(mockedFunctionReturn2.value).toBe(0); + + expect(mockFunction).toHaveBeenCalledTimes(2); + }); + + it("caches entries in matrix properly with multiple arguments", () => { + const mockFunction = jest.fn(); + + const getter = Matrix.autoMockMethod(mockFunction, () => { + return new TestObject(); + }); + + const obj = getter("test1", 4); + expect(obj.value).toBe(0); + + obj.increment(); + + expect(getter("test1", 4).value).toBe(1); + + expect(mockFunction("test1", 3).value).toBe(0); + }); + + it("should give original args in creator even if it has multiple key layers", () => { + const mockFunction = jest.fn(); + + let invoked = false; + + const getter = Matrix.autoMockMethod(mockFunction, (args) => { + expect(args).toHaveLength(3); + expect(args[0]).toBe("test"); + expect(args[1]).toBe(42); + expect(args[2]).toBe(true); + + invoked = true; + + return new TestObject(); + }); + + getter("test", 42, true); + expect(invoked).toBe(true); + }); +}); diff --git a/libs/common/spec/matrix.ts b/libs/common/spec/matrix.ts new file mode 100644 index 00000000000..e4ac9f5537f --- /dev/null +++ b/libs/common/spec/matrix.ts @@ -0,0 +1,115 @@ +type PickFirst = Array extends [infer First, ...unknown[]] ? First : never; + +type MatrixOrValue = Array extends [] + ? Value + : Matrix; + +type RemoveFirst = T extends [unknown, ...infer Rest] ? Rest : never; + +/** + * A matrix is intended to manage cached values for a set of method arguments. + */ +export class Matrix { + private map: Map, MatrixOrValue, TValue>> = new Map(); + + /** + * This is especially useful for methods on a service that take inputs but return Observables. + * Generally when interacting with observables in tests, you want to use a simple SubjectLike + * type to back it instead, so that you can easily `next` values to simulate an emission. + * + * @param mockFunction The function to have a Matrix based implementation added to it. + * @param creator The function to use to create the underlying value to return for the given arguments. + * @returns A "getter" function that allows you to retrieve the backing value that is used for the given arguments. + * + * @example + * ```ts + * interface MyService { + * event$(userId: UserId) => Observable + * } + * + * // Test + * const myService = mock(); + * const eventGetter = Matrix.autoMockMethod(myService.event$, (userId) => BehaviorSubject()); + * + * eventGetter("userOne").next(new UserEvent()); + * eventGetter("userTwo").next(new UserEvent()); + * ``` + * + * This replaces a more manual way of doing things like: + * + * ```ts + * const myService = mock(); + * const userOneSubject = new BehaviorSubject(); + * const userTwoSubject = new BehaviorSubject(); + * myService.event$.mockImplementation((userId) => { + * if (userId === "userOne") { + * return userOneSubject; + * } else if (userId === "userTwo") { + * return userTwoSubject; + * } + * return new BehaviorSubject(); + * }); + * + * userOneSubject.next(new UserEvent()); + * userTwoSubject.next(new UserEvent()); + * ``` + */ + static autoMockMethod( + mockFunction: jest.Mock, + creator: (args: TArgs) => TActualReturn, + ): (...args: TArgs) => TActualReturn { + const matrix = new Matrix(); + + const getter = (...args: TArgs) => { + return matrix.getOrCreateEntry(args, creator); + }; + + mockFunction.mockImplementation(getter); + + return getter; + } + + /** + * Gives the ability to get or create an entry in the matrix via the given args. + * + * @note The args are evaulated using Javascript equality so primivites work best. + * + * @param args The arguments to use to evaluate if an entry in the matrix exists already, + * or a value should be created and stored with those arguments. + * @param creator The function to call with the arguments to build a value. + * @returns The existing entry if one already exists or a new value created with the creator param. + */ + getOrCreateEntry(args: TKeys, creator: (args: TKeys) => TValue): TValue { + if (args.length === 0) { + throw new Error("Matrix is not for you."); + } + + if (args.length === 1) { + const arg = args[0] as PickFirst; + if (this.map.has(arg)) { + // Get the cached value + return this.map.get(arg) as TValue; + } else { + const value = creator(args); + // Save the value for the next time + this.map.set(arg, value as MatrixOrValue, TValue>); + return value; + } + } + + // There are for sure 2 or more args + const [first, ...rest] = args as unknown as [PickFirst, ...RemoveFirst]; + + let matrix: Matrix, TValue> | null = null; + + if (this.map.has(first)) { + // We've already created a map for this argument + matrix = this.map.get(first) as Matrix, TValue>; + } else { + matrix = new Matrix, TValue>(); + this.map.set(first, matrix as MatrixOrValue, TValue>); + } + + return matrix.getOrCreateEntry(rest, () => creator(args)); + } +} diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index daccc4bd16e..5bd2221860b 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -70,6 +70,7 @@ import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; @@ -151,7 +152,12 @@ export abstract class ApiService { | SsoTokenRequest | UserApiTokenRequest | WebAuthnLoginTokenRequest, - ) => Promise; + ) => Promise< + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse + >; refreshIdentityToken: () => Promise; getProfile: () => Promise; diff --git a/libs/common/src/abstractions/notifications.service.ts b/libs/common/src/abstractions/notifications.service.ts deleted file mode 100644 index 2234a5588a6..00000000000 --- a/libs/common/src/abstractions/notifications.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -export abstract class NotificationsService { - init: () => Promise; - updateConnection: (sync?: boolean) => Promise; - reconnectFromActivity: () => Promise; - disconnectFromInactivity: () => Promise; -} diff --git a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts index da81f340fda..05c214ece13 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts @@ -57,14 +57,6 @@ export function getOrganizationById(id: string) { return map((orgs) => orgs.find((o) => o.id === id)); } -/** - * Returns `true` if a user is a member of an organization (rather than only being a ProviderUser) - * @deprecated Use organizationService.organizations$ with a filter instead - */ -export function isMember(org: Organization): boolean { - return org.isMember; -} - /** * Publishes an observable stream of organizations. This service is meant to * be used widely across Bitwarden as the primary way of fetching organizations. @@ -73,41 +65,23 @@ export function isMember(org: Organization): boolean { */ export abstract class OrganizationService { /** - * Publishes state for all organizations under the active user. + * Publishes state for all organizations under the specified user. * @returns An observable list of organizations */ - organizations$: Observable; + organizations$: (userId: UserId) => Observable; // @todo Clean these up. Continuing to expand them is not recommended. // @see https://bitwarden.atlassian.net/browse/AC-2252 - memberOrganizations$: Observable; - /** - * @deprecated This is currently only used in the CLI, and should not be - * used in any new calls. Use get$ instead for the time being, and we'll be - * removing this method soon. See Jira for details: - * https://bitwarden.atlassian.net/browse/AC-2252. - */ - getFromState: (id: string) => Promise; + memberOrganizations$: (userId: UserId) => Observable; /** * Emits true if the user can create or manage a Free Bitwarden Families sponsorship. */ - canManageSponsorships$: Observable; + canManageSponsorships$: (userId: UserId) => Observable; /** * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available. */ - familySponsorshipAvailable$: Observable; - hasOrganizations: () => Promise; - get$: (id: string) => Observable; - get: (id: string) => Promise; - /** - * @deprecated This method is only used in key connector and will be removed soon as part of https://bitwarden.atlassian.net/browse/AC-2252. - */ - getAll: (userId?: string) => Promise; - - /** - * Publishes state for all organizations for the given user id or the active user. - */ - getAll$: (userId?: UserId) => Observable; + familySponsorshipAvailable$: (userId: UserId) => Observable; + hasOrganizations: (userId: UserId) => Observable; } /** @@ -120,20 +94,18 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio /** * Replaces state for the provided organization, or creates it if not found. * @param organization The organization state being saved. - * @param userId The userId to replace state for. Defaults to the active - * user. + * @param userId The userId to replace state for. */ - upsert: (OrganizationData: OrganizationData) => Promise; + upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise; /** - * Replaces state for the entire registered organization list for the active user. + * Replaces state for the entire registered organization list for the specified user. * You probably don't want this unless you're calling from a full sync * operation or a logout. See `upsert` for creating & updating a single * organization in the state. - * @param organizations A complete list of all organization state for the active - * user. - * @param userId The userId to replace state for. Defaults to the active + * @param organizations A complete list of all organization state for the provided * user. + * @param userId The userId to replace state for. */ - replace: (organizations: { [id: string]: OrganizationData }, userId?: UserId) => Promise; + replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise; } diff --git a/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts b/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts deleted file mode 100644 index c25a153a068..00000000000 --- a/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts +++ /dev/null @@ -1,111 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { map, Observable } from "rxjs"; - -import { UserId } from "../../../types/guid"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -export function canAccessVaultTab(org: Organization): boolean { - return org.canViewAllCollections; -} - -export function canAccessSettingsTab(org: Organization): boolean { - return ( - org.isOwner || - org.canManagePolicies || - org.canManageSso || - org.canManageScim || - org.canAccessImport || - org.canAccessExport || - org.canManageDeviceApprovals - ); -} - -export function canAccessMembersTab(org: Organization): boolean { - return org.canManageUsers || org.canManageUsersPassword; -} - -export function canAccessGroupsTab(org: Organization): boolean { - return org.canManageGroups; -} - -export function canAccessReportingTab(org: Organization): boolean { - return org.canAccessReports || org.canAccessEventLogs; -} - -export function canAccessBillingTab(org: Organization): boolean { - return org.isOwner; -} - -export function canAccessOrgAdmin(org: Organization): boolean { - // Admin console can only be accessed by Owners for disabled organizations - if (!org.enabled && !org.isOwner) { - return false; - } - return ( - canAccessMembersTab(org) || - canAccessGroupsTab(org) || - canAccessReportingTab(org) || - canAccessBillingTab(org) || - canAccessSettingsTab(org) || - canAccessVaultTab(org) - ); -} - -export function getOrganizationById(id: string) { - return map((orgs) => orgs.find((o) => o.id === id)); -} - -/** - * Publishes an observable stream of organizations. This service is meant to - * be used widely across Bitwarden as the primary way of fetching organizations. - * Risky operations like updates are isolated to the - * internal extension `InternalOrganizationServiceAbstraction`. - */ -export abstract class vNextOrganizationService { - /** - * Publishes state for all organizations under the specified user. - * @returns An observable list of organizations - */ - organizations$: (userId: UserId) => Observable; - - // @todo Clean these up. Continuing to expand them is not recommended. - // @see https://bitwarden.atlassian.net/browse/AC-2252 - memberOrganizations$: (userId: UserId) => Observable; - /** - * Emits true if the user can create or manage a Free Bitwarden Families sponsorship. - */ - canManageSponsorships$: (userId: UserId) => Observable; - /** - * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available. - */ - familySponsorshipAvailable$: (userId: UserId) => Observable; - hasOrganizations: (userId: UserId) => Observable; -} - -/** - * Big scary buttons that **update** organization state. These should only be - * called from within admin-console scoped code. Extends the base - * `OrganizationService` for easy access to `get` calls. - * @internal - */ -export abstract class vNextInternalOrganizationServiceAbstraction extends vNextOrganizationService { - /** - * Replaces state for the provided organization, or creates it if not found. - * @param organization The organization state being saved. - * @param userId The userId to replace state for. - */ - upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise; - - /** - * Replaces state for the entire registered organization list for the specified user. - * You probably don't want this unless you're calling from a full sync - * operation or a logout. See `upsert` for creating & updating a single - * organization in the state. - * @param organizations A complete list of all organization state for the provided - * user. - * @param userId The userId to replace state for. - */ - replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise; -} diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index bed341115ee..4280756326c 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -30,7 +30,7 @@ export abstract class PolicyService { * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). * @param policyType the {@link PolicyType} to search for */ - getAll$: (policyType: PolicyType, userId?: UserId) => Observable; + getAll$: (policyType: PolicyType, userId: UserId) => Observable; /** * All {@link Policy} objects for the specified user (from sync data). diff --git a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts index f348e7487de..ffe79f0ad3b 100644 --- a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; + import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request"; import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; @@ -14,4 +16,12 @@ export class ProviderApiServiceAbstraction { request: ProviderVerifyRecoverDeleteRequest, ) => Promise; deleteProvider: (id: string) => Promise; + getProviderAddableOrganizations: (providerId: string) => Promise; + addOrganizationToProvider: ( + providerId: string, + request: { + key: string; + organizationId: string; + }, + ) => Promise; } diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts index da9a82e7c5c..5f487e1f898 100644 --- a/libs/common/src/admin-console/models/data/organization.data.spec.ts +++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts @@ -1,6 +1,6 @@ import { ProductTierType } from "../../../billing/enums/product-tier-type.enum"; import { OrganizationUserStatusType, OrganizationUserType } from "../../enums"; -import { ORGANIZATIONS } from "../../services/organization/organization.service"; +import { ORGANIZATIONS } from "../../services/organization/organization.state"; import { OrganizationData } from "./organization.data"; @@ -53,6 +53,7 @@ describe("ORGANIZATIONS state", () => { accessSecretsManager: false, limitCollectionCreation: false, limitCollectionDeletion: false, + limitItemDeletion: false, allowAdminAccessToAllCollectionItems: false, familySponsorshipLastSyncDate: new Date(), userIsManagedByOrganization: false, diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts index 8ec84b5fd09..b81d06e6367 100644 --- a/libs/common/src/admin-console/models/data/organization.data.ts +++ b/libs/common/src/admin-console/models/data/organization.data.ts @@ -56,6 +56,7 @@ export class OrganizationData { accessSecretsManager: boolean; limitCollectionCreation: boolean; limitCollectionDeletion: boolean; + limitItemDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; useRiskInsights: boolean; @@ -117,6 +118,7 @@ export class OrganizationData { this.accessSecretsManager = response.accessSecretsManager; this.limitCollectionCreation = response.limitCollectionCreation; this.limitCollectionDeletion = response.limitCollectionDeletion; + this.limitItemDeletion = response.limitItemDeletion; this.allowAdminAccessToAllCollectionItems = response.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = response.userIsManagedByOrganization; this.useRiskInsights = response.useRiskInsights; diff --git a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts index 1f8c4e8c42d..5f8fe3349b6 100644 --- a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts +++ b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts @@ -1,4 +1,4 @@ -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { OrgKey, UserPrivateKey } from "../../../types/key"; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 9dcc9f0752c..6f7ff561f04 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -76,6 +76,12 @@ export class Organization { /** * Refers to the ability for an owner/admin to access all collection items, regardless of assigned collections */ + limitItemDeletion: boolean; + /** + * Refers to the ability to limit delete permission of collection items. + * If set to true, members can only delete items when they have a Can Manage permission over the collection. + * If set to false, members can delete items when they have a Can Manage OR Can Edit permission over the collection. + */ allowAdminAccessToAllCollectionItems: boolean; /** * Indicates if this organization manages the user. @@ -138,6 +144,7 @@ export class Organization { this.accessSecretsManager = obj.accessSecretsManager; this.limitCollectionCreation = obj.limitCollectionCreation; this.limitCollectionDeletion = obj.limitCollectionDeletion; + this.limitItemDeletion = obj.limitItemDeletion; this.allowAdminAccessToAllCollectionItems = obj.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = obj.userIsManagedByOrganization; this.useRiskInsights = obj.useRiskInsights; diff --git a/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts b/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts index 23c39376d71..2545a725598 100644 --- a/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts +++ b/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts @@ -3,5 +3,6 @@ export class OrganizationCollectionManagementUpdateRequest { limitCollectionCreation: boolean; limitCollectionDeletion: boolean; + limitItemDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; } diff --git a/libs/common/src/admin-console/models/response/addable-organization.response.ts b/libs/common/src/admin-console/models/response/addable-organization.response.ts new file mode 100644 index 00000000000..74ae5f45690 --- /dev/null +++ b/libs/common/src/admin-console/models/response/addable-organization.response.ts @@ -0,0 +1,18 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class AddableOrganizationResponse extends BaseResponse { + id: string; + plan: string; + name: string; + seats: number; + disabled: boolean; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("id"); + this.plan = this.getResponseProperty("plan"); + this.name = this.getResponseProperty("name"); + this.seats = this.getResponseProperty("seats"); + this.disabled = this.getResponseProperty("disabled"); + } +} diff --git a/libs/common/src/admin-console/models/response/organization.response.ts b/libs/common/src/admin-console/models/response/organization.response.ts index fd54ff128b6..235ea2f8d96 100644 --- a/libs/common/src/admin-console/models/response/organization.response.ts +++ b/libs/common/src/admin-console/models/response/organization.response.ts @@ -36,6 +36,7 @@ export class OrganizationResponse extends BaseResponse { maxAutoscaleSmServiceAccounts?: number; limitCollectionCreation: boolean; limitCollectionDeletion: boolean; + limitItemDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; useRiskInsights: boolean; @@ -75,6 +76,7 @@ export class OrganizationResponse extends BaseResponse { this.maxAutoscaleSmServiceAccounts = this.getResponseProperty("MaxAutoscaleSmServiceAccounts"); this.limitCollectionCreation = this.getResponseProperty("LimitCollectionCreation"); this.limitCollectionDeletion = this.getResponseProperty("LimitCollectionDeletion"); + this.limitItemDeletion = this.getResponseProperty("LimitItemDeletion"); this.allowAdminAccessToAllCollectionItems = this.getResponseProperty( "AllowAdminAccessToAllCollectionItems", ); diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts index 9c4b8885ab8..5e37cfc4c5c 100644 --- a/libs/common/src/admin-console/models/response/profile-organization.response.ts +++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts @@ -51,6 +51,7 @@ export class ProfileOrganizationResponse extends BaseResponse { accessSecretsManager: boolean; limitCollectionCreation: boolean; limitCollectionDeletion: boolean; + limitItemDeletion: boolean; allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; useRiskInsights: boolean; @@ -114,6 +115,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager"); this.limitCollectionCreation = this.getResponseProperty("LimitCollectionCreation"); this.limitCollectionDeletion = this.getResponseProperty("LimitCollectionDeletion"); + this.limitItemDeletion = this.getResponseProperty("LimitItemDeletion"); this.allowAdminAccessToAllCollectionItems = this.getResponseProperty( "AllowAdminAccessToAllCollectionItems", ); diff --git a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts b/libs/common/src/admin-console/services/organization/default-organization.service.spec.ts similarity index 96% rename from libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts rename to libs/common/src/admin-console/services/organization/default-organization.service.spec.ts index 9e2ea3a4599..41c89c0e41a 100644 --- a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts +++ b/libs/common/src/admin-console/services/organization/default-organization.service.spec.ts @@ -6,11 +6,11 @@ import { OrganizationId, UserId } from "../../../types/guid"; import { OrganizationData } from "../../models/data/organization.data"; import { Organization } from "../../models/domain/organization"; -import { DefaultvNextOrganizationService } from "./default-vnext-organization.service"; -import { ORGANIZATIONS } from "./vnext-organization.state"; +import { DefaultOrganizationService } from "./default-organization.service"; +import { ORGANIZATIONS } from "./organization.state"; describe("OrganizationService", () => { - let organizationService: DefaultvNextOrganizationService; + let organizationService: DefaultOrganizationService; const fakeUserId = Utils.newGuid() as UserId; let fakeStateProvider: FakeStateProvider; @@ -86,7 +86,7 @@ describe("OrganizationService", () => { beforeEach(async () => { fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(fakeUserId)); - organizationService = new DefaultvNextOrganizationService(fakeStateProvider); + organizationService = new DefaultOrganizationService(fakeStateProvider); }); describe("canManageSponsorships", () => { diff --git a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts b/libs/common/src/admin-console/services/organization/default-organization.service.ts similarity index 92% rename from libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts rename to libs/common/src/admin-console/services/organization/default-organization.service.ts index 8b73c271daf..e78136455fd 100644 --- a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts +++ b/libs/common/src/admin-console/services/organization/default-organization.service.ts @@ -4,11 +4,11 @@ import { map, Observable } from "rxjs"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; -import { vNextInternalOrganizationServiceAbstraction } from "../../abstractions/organization/vnext.organization.service"; +import { InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "../../models/data/organization.data"; import { Organization } from "../../models/domain/organization"; -import { ORGANIZATIONS } from "./vnext-organization.state"; +import { ORGANIZATIONS } from "./organization.state"; /** * Filter out organizations from an observable that __do not__ offer a @@ -41,9 +41,7 @@ function mapToBooleanHasAnyOrganizations() { return map((orgs) => orgs.length > 0); } -export class DefaultvNextOrganizationService - implements vNextInternalOrganizationServiceAbstraction -{ +export class DefaultOrganizationService implements InternalOrganizationServiceAbstraction { memberOrganizations$(userId: UserId): Observable { return this.organizations$(userId).pipe(mapToExcludeProviderOrganizations()); } diff --git a/libs/common/src/admin-console/services/organization/organization.service.spec.ts b/libs/common/src/admin-console/services/organization/organization.service.spec.ts deleted file mode 100644 index 6d2525966bc..00000000000 --- a/libs/common/src/admin-console/services/organization/organization.service.spec.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { firstValueFrom } from "rxjs"; - -import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { FakeActiveUserState } from "../../../../spec/fake-state"; -import { Utils } from "../../../platform/misc/utils"; -import { OrganizationId, UserId } from "../../../types/guid"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -import { OrganizationService, ORGANIZATIONS } from "./organization.service"; - -describe("OrganizationService", () => { - let organizationService: OrganizationService; - - const fakeUserId = Utils.newGuid() as UserId; - let fakeAccountService: FakeAccountService; - let fakeStateProvider: FakeStateProvider; - let fakeActiveUserState: FakeActiveUserState>; - - /** - * It is easier to read arrays than records in code, but we store a record - * in state. This helper methods lets us build organization arrays in tests - * and easily map them to records before storing them in state. - */ - function arrayToRecord(input: OrganizationData[]): Record { - if (input == null) { - return undefined; - } - return Object.fromEntries(input?.map((i) => [i.id, i])); - } - - /** - * There are a few assertions in this spec that check for array equality - * but want to ignore a specific index that _should_ be different. This - * function takes two arrays, and an index. It checks for equality of the - * arrays, but splices out the specified index from both arrays first. - */ - function expectIsEqualExceptForIndex(x: any[], y: any[], indexToExclude: number) { - // Clone the arrays to avoid modifying the reference values - const a = [...x]; - const b = [...y]; - delete a[indexToExclude]; - delete b[indexToExclude]; - expect(a).toEqual(b); - } - - /** - * Builds a simple mock `OrganizationData[]` array that can be used in tests - * to populate state. - * @param count The number of organizations to populate the list with. The - * function returns undefined if this is less than 1. The default value is 1. - * @param suffix A string to append to data fields on each organization. - * This defaults to the index of the organization in the list. - * @returns an `OrganizationData[]` array that can be used to populate - * stateProvider. - */ - function buildMockOrganizations(count = 1, suffix?: string): OrganizationData[] { - if (count < 1) { - return undefined; - } - - function buildMockOrganization(id: OrganizationId, name: string, identifier: string) { - const data = new OrganizationData({} as any, {} as any); - data.id = id; - data.name = name; - data.identifier = identifier; - - return data; - } - - const mockOrganizations = []; - for (let i = 0; i < count; i++) { - const s = suffix ? suffix + i.toString() : i.toString(); - mockOrganizations.push( - buildMockOrganization(("org" + s) as OrganizationId, "org" + s, "orgIdentifier" + s), - ); - } - - return mockOrganizations; - } - - /** - * `OrganizationService` deals with multiple accounts at times. This helper - * function can be used to add a new non-active account to the test data. - * This function is **not** needed to handle creation of the first account, - * as that is handled by the `FakeAccountService` in `mockAccountServiceWith()` - * @returns The `UserId` of the newly created state account and the mock data - * created for them as an `Organization[]`. - */ - async function addNonActiveAccountToStateProvider(): Promise<[UserId, OrganizationData[]]> { - const nonActiveUserId = Utils.newGuid() as UserId; - - const mockOrganizations = buildMockOrganizations(10); - const fakeNonActiveUserState = fakeStateProvider.singleUser.getFake( - nonActiveUserId, - ORGANIZATIONS, - ); - fakeNonActiveUserState.nextState(arrayToRecord(mockOrganizations)); - - return [nonActiveUserId, mockOrganizations]; - } - - beforeEach(async () => { - fakeAccountService = mockAccountServiceWith(fakeUserId); - fakeStateProvider = new FakeStateProvider(fakeAccountService); - fakeActiveUserState = fakeStateProvider.activeUser.getFake(ORGANIZATIONS); - organizationService = new OrganizationService(fakeStateProvider); - }); - - it("getAll", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const orgs = await organizationService.getAll(); - expect(orgs).toHaveLength(1); - const org = orgs[0]; - expect(org).toEqual(new Organization(mockData[0])); - }); - - describe("canManageSponsorships", () => { - it("can because one is available", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipAvailable = true; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(true); - }); - - it("can because one is used", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipFriendlyName = "Something"; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(true); - }); - - it("can not because one isn't available or taken", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipFriendlyName = null; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(false); - }); - }); - - describe("get", () => { - it("exists", async () => { - const mockData = buildMockOrganizations(1); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await organizationService.get(mockData[0].id); - expect(result).toEqual(new Organization(mockData[0])); - }); - - it("does not exist", async () => { - const result = await organizationService.get("this-org-does-not-exist"); - expect(result).toBe(undefined); - }); - }); - - describe("organizations$", () => { - describe("null checking behavior", () => { - it("publishes an empty array if organizations in state = undefined", async () => { - const mockData: OrganizationData[] = undefined; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - - it("publishes an empty array if organizations in state = null", async () => { - const mockData: OrganizationData[] = null; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - - it("publishes an empty array if organizations in state = []", async () => { - const mockData: OrganizationData[] = []; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - }); - - describe("parameter handling & returns", () => { - it("publishes all organizations for the active user by default", async () => { - const mockData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual(mockData); - }); - - it("can be used to publish the organizations of a non active user if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserState"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, nonActiveUserMockOrganizations] = - await addNonActiveAccountToStateProvider(); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - - expect(result).toEqual(nonActiveUserMockOrganizations); - expect(result).not.toEqual(await firstValueFrom(organizationService.organizations$)); - }); - }); - }); - - describe("upsert()", () => { - it("can create the organization list if necassary", async () => { - // Notice that no default state is provided in this test, so the list in - // `stateProvider` will be null when the `upsert` method is called. - const mockData = buildMockOrganizations(); - await organizationService.upsert(mockData[0]); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual(mockData.map((x) => new Organization(x))); - }); - - it("updates an organization that already exists in state, defaulting to the active user", async () => { - const mockData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const indexToUpdate = 5; - const anUpdatedOrganization = { - ...buildMockOrganizations(1, "UPDATED").pop(), - id: mockData[indexToUpdate].id, - }; - await organizationService.upsert(anUpdatedOrganization); - const result = await firstValueFrom(organizationService.organizations$); - expect(result[indexToUpdate]).not.toEqual(new Organization(mockData[indexToUpdate])); - expect(result[indexToUpdate].id).toEqual(new Organization(mockData[indexToUpdate]).id); - expectIsEqualExceptForIndex( - result, - mockData.map((x) => new Organization(x)), - indexToUpdate, - ); - }); - - it("can also update an organization in state for a non-active user, if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserOrganizations"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, nonActiveUserMockOrganizations] = - await addNonActiveAccountToStateProvider(); - const indexToUpdate = 5; - const anUpdatedOrganization = { - ...buildMockOrganizations(1, "UPDATED").pop(), - id: nonActiveUserMockOrganizations[indexToUpdate].id, - }; - - await organizationService.upsert(anUpdatedOrganization, nonActiveUserId); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - - expect(result[indexToUpdate]).not.toEqual( - new Organization(nonActiveUserMockOrganizations[indexToUpdate]), - ); - expect(result[indexToUpdate].id).toEqual( - new Organization(nonActiveUserMockOrganizations[indexToUpdate]).id, - ); - expectIsEqualExceptForIndex( - result, - nonActiveUserMockOrganizations.map((x) => new Organization(x)), - indexToUpdate, - ); - - // Just to be safe, lets make sure the active user didn't get updated - // at all - const activeUserState = await firstValueFrom(organizationService.organizations$); - expect(activeUserState).toEqual(activeUserMockData.map((x) => new Organization(x))); - expect(activeUserState).not.toEqual(result); - }); - }); - - describe("replace()", () => { - it("replaces the entire organization list in state, defaulting to the active user", async () => { - const originalData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(originalData)); - - const newData = buildMockOrganizations(10, "newData"); - await organizationService.replace(arrayToRecord(newData)); - - const result = await firstValueFrom(organizationService.organizations$); - - expect(result).toEqual(newData); - expect(result).not.toEqual(originalData); - }); - - // This is more or less a test for logouts - it("can replace state with null", async () => { - const originalData = buildMockOrganizations(2); - fakeActiveUserState.nextState(arrayToRecord(originalData)); - await organizationService.replace(null); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - expect(result).not.toEqual(originalData); - }); - - it("can also replace state for a non-active user, if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserOrganizations"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, originalOrganizations] = await addNonActiveAccountToStateProvider(); - const newData = buildMockOrganizations(10, "newData"); - - await organizationService.replace(arrayToRecord(newData), nonActiveUserId); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - expect(result).toEqual(newData); - expect(result).not.toEqual(originalOrganizations); - - // Just to be safe, lets make sure the active user didn't get updated - // at all - const activeUserState = await firstValueFrom(organizationService.organizations$); - expect(activeUserState).toEqual(activeUserMockData.map((x) => new Organization(x))); - expect(activeUserState).not.toEqual(result); - }); - }); -}); diff --git a/libs/common/src/admin-console/services/organization/organization.service.ts b/libs/common/src/admin-console/services/organization/organization.service.ts deleted file mode 100644 index 49e906bdac2..00000000000 --- a/libs/common/src/admin-console/services/organization/organization.service.ts +++ /dev/null @@ -1,160 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { map, Observable, firstValueFrom } from "rxjs"; -import { Jsonify } from "type-fest"; - -import { ORGANIZATIONS_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; -import { InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -/** - * The `KeyDefinition` for accessing organization lists in application state. - * @todo Ideally this wouldn't require a `fromJSON()` call, but `OrganizationData` - * has some properties that contain functions. This should probably get - * cleaned up. - */ -export const ORGANIZATIONS = UserKeyDefinition.record( - ORGANIZATIONS_DISK, - "organizations", - { - deserializer: (obj: Jsonify) => OrganizationData.fromJSON(obj), - clearOn: ["logout"], - }, -); - -/** - * Filter out organizations from an observable that __do not__ offer a - * families-for-enterprise sponsorship to members. - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToExcludeOrganizationsWithoutFamilySponsorshipSupport() { - return map((orgs) => orgs.filter((o) => o.canManageSponsorships)); -} - -/** - * Filter out organizations from an observable that the organization user - * __is not__ a direct member of. This will exclude organizations only - * accessible as a provider. - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToExcludeProviderOrganizations() { - return map((orgs) => orgs.filter((o) => o.isMember)); -} - -/** - * Map an observable stream of organizations down to a boolean indicating - * if any organizations exist (`orgs.length > 0`). - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToBooleanHasAnyOrganizations() { - return map((orgs) => orgs.length > 0); -} - -/** - * Map an observable stream of organizations down to a single organization. - * @param `organizationId` The ID of the organization you'd like to subscribe to - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToSingleOrganization(organizationId: string) { - return map((orgs) => orgs?.find((o) => o.id === organizationId)); -} - -export class OrganizationService implements InternalOrganizationServiceAbstraction { - organizations$: Observable = this.getOrganizationsFromState$(); - memberOrganizations$: Observable = this.organizations$.pipe( - mapToExcludeProviderOrganizations(), - ); - - constructor(private stateProvider: StateProvider) {} - - get$(id: string): Observable { - return this.organizations$.pipe(mapToSingleOrganization(id)); - } - - getAll$(userId?: UserId): Observable { - return this.getOrganizationsFromState$(userId); - } - - async getAll(userId?: string): Promise { - return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId)); - } - - canManageSponsorships$ = this.organizations$.pipe( - mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(), - mapToBooleanHasAnyOrganizations(), - ); - - familySponsorshipAvailable$ = this.organizations$.pipe( - map((orgs) => orgs.some((o) => o.familySponsorshipAvailable)), - ); - - async hasOrganizations(): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations())); - } - - async upsert(organization: OrganizationData, userId?: UserId): Promise { - await this.stateFor(userId).update((existingOrganizations) => { - const organizations = existingOrganizations ?? {}; - organizations[organization.id] = organization; - return organizations; - }); - } - - async get(id: string): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToSingleOrganization(id))); - } - - /** - * @deprecated For the CLI only - * @param id id of the organization - */ - async getFromState(id: string): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToSingleOrganization(id))); - } - - async replace(organizations: { [id: string]: OrganizationData }, userId?: UserId): Promise { - await this.stateFor(userId).update(() => organizations); - } - - // Ideally this method would be renamed to organizations$() and the - // $organizations observable as it stands would be removed. This will - // require updates to callers, and so this method exists as a temporary - // workaround until we have time & a plan to update callers. - // - // It can be thought of as "organizations$ but with a userId option". - private getOrganizationsFromState$(userId?: UserId): Observable { - return this.stateFor(userId).state$.pipe(this.mapOrganizationRecordToArray()); - } - - /** - * Accepts a record of `OrganizationData`, which is how we store the - * organization list as a JSON object on disk, to an array of - * `Organization`, which is how the data is published to callers of the - * service. - * @returns a function that can be used to pipe organization data from - * stored state to an exposed object easily consumable by others. - */ - private mapOrganizationRecordToArray() { - return map, Organization[]>((orgs) => - Object.values(orgs ?? {})?.map((o) => new Organization(o)), - ); - } - - /** - * Fetches the organization list from on disk state for the specified user. - * @param userId the user ID to fetch the organization list for. Defaults to - * the currently active user. - * @returns an observable of organization state as it is stored on disk. - */ - private stateFor(userId?: UserId) { - return userId - ? this.stateProvider.getUser(userId, ORGANIZATIONS) - : this.stateProvider.getActive(ORGANIZATIONS); - } -} diff --git a/libs/common/src/admin-console/services/organization/vnext-organization.state.ts b/libs/common/src/admin-console/services/organization/organization.state.ts similarity index 100% rename from libs/common/src/admin-console/services/organization/vnext-organization.state.ts rename to libs/common/src/admin-console/services/organization/organization.state.ts diff --git a/libs/common/src/admin-console/services/policy/policy.service.spec.ts b/libs/common/src/admin-console/services/policy/policy.service.spec.ts index d9802db9e38..12b57f1b4f7 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.spec.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.spec.ts @@ -2,8 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { FakeActiveUserState } from "../../../../spec/fake-state"; -import { OrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; +import { FakeActiveUserState, FakeSingleUserState } from "../../../../spec/fake-state"; import { OrganizationUserStatusType, OrganizationUserType, @@ -18,12 +17,14 @@ import { Policy } from "../../../admin-console/models/domain/policy"; import { ResetPasswordPolicyOptions } from "../../../admin-console/models/domain/reset-password-policy-options"; import { POLICIES, PolicyService } from "../../../admin-console/services/policy/policy.service"; import { PolicyId, UserId } from "../../../types/guid"; +import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; describe("PolicyService", () => { const userId = "userId" as UserId; let stateProvider: FakeStateProvider; let organizationService: MockProxy; let activeUserState: FakeActiveUserState>; + let singleUserState: FakeSingleUserState>; let policyService: PolicyService; @@ -33,6 +34,7 @@ describe("PolicyService", () => { organizationService = mock(); activeUserState = stateProvider.activeUser.getFake(POLICIES); + singleUserState = stateProvider.singleUser.getFake(activeUserState.userId, POLICIES); const organizations$ = of([ // User @@ -56,9 +58,7 @@ describe("PolicyService", () => { organization("org6", true, true, OrganizationUserStatusType.Confirmed, true), ]); - organizationService.organizations$ = organizations$; - - organizationService.getAll$.mockReturnValue(organizations$); + organizationService.organizations$.mockReturnValue(organizations$); policyService = new PolicyService(stateProvider, organizationService); }); @@ -196,7 +196,7 @@ describe("PolicyService", () => { describe("getResetPasswordPolicyOptions", () => { it("default", async () => { - const result = policyService.getResetPasswordPolicyOptions(null, null); + const result = policyService.getResetPasswordPolicyOptions([], ""); expect(result).toEqual([new ResetPasswordPolicyOptions(), false]); }); @@ -297,7 +297,7 @@ describe("PolicyService", () => { describe("getAll$", () => { it("returns the specified PolicyTypes", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -307,7 +307,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -333,7 +333,7 @@ describe("PolicyService", () => { }); it("does not return disabled policies", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -343,7 +343,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -363,7 +363,7 @@ describe("PolicyService", () => { }); it("does not return policies that do not apply to the user because the user's role is exempt", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -373,7 +373,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ @@ -393,7 +393,7 @@ describe("PolicyService", () => { }); it("does not return policies for organizations that do not use policies", async () => { - activeUserState.nextState( + singleUserState.nextState( arrayToRecord([ policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), policyData("policy2", "org1", PolicyType.ActivateAutofill, true), @@ -403,7 +403,7 @@ describe("PolicyService", () => { ); const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport), + policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), ); expect(result).toEqual([ diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts index 7a04ba38aa7..3378d2021ef 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { combineLatest, firstValueFrom, map, Observable, of } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; import { UserKeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state"; import { PolicyId, UserId } from "../../../types/guid"; @@ -39,7 +39,11 @@ export class PolicyService implements InternalPolicyServiceAbstraction { map((policies) => policies.filter((p) => p.type === policyType)), ); - return combineLatest([filteredPolicies$, this.organizationService.organizations$]).pipe( + const organizations$ = this.stateProvider.activeUserId$.pipe( + switchMap((userId) => this.organizationService.organizations$(userId)), + ); + + return combineLatest([filteredPolicies$, organizations$]).pipe( map( ([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)?.at(0) ?? null, @@ -47,13 +51,13 @@ export class PolicyService implements InternalPolicyServiceAbstraction { ); } - getAll$(policyType: PolicyType, userId?: UserId) { + getAll$(policyType: PolicyType, userId: UserId) { const filteredPolicies$ = this.stateProvider.getUserState$(POLICIES, userId).pipe( map((policyData) => policyRecordToArray(policyData)), map((policies) => policies.filter((p) => p.type === policyType)), ); - return combineLatest([filteredPolicies$, this.organizationService.getAll$(userId)]).pipe( + return combineLatest([filteredPolicies$, this.organizationService.organizations$(userId)]).pipe( map(([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)), ); } diff --git a/libs/common/src/admin-console/services/provider/provider-api.service.ts b/libs/common/src/admin-console/services/provider/provider-api.service.ts index 2ee921393ff..dc82ec011f4 100644 --- a/libs/common/src/admin-console/services/provider/provider-api.service.ts +++ b/libs/common/src/admin-console/services/provider/provider-api.service.ts @@ -1,3 +1,5 @@ +import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; + import { ApiService } from "../../../abstractions/api.service"; import { ProviderApiServiceAbstraction } from "../../abstractions/provider/provider-api.service.abstraction"; import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; @@ -44,4 +46,34 @@ export class ProviderApiService implements ProviderApiServiceAbstraction { async deleteProvider(id: string): Promise { await this.apiService.send("DELETE", "/providers/" + id, null, true, false); } + + async getProviderAddableOrganizations( + providerId: string, + ): Promise { + const response = await this.apiService.send( + "GET", + "/providers/" + providerId + "/clients/addable", + null, + true, + true, + ); + + return response.map((data: any) => new AddableOrganizationResponse(data)); + } + + addOrganizationToProvider( + providerId: string, + request: { + key: string; + organizationId: string; + }, + ): Promise { + return this.apiService.send( + "POST", + "/providers/" + providerId + "/clients/existing", + request, + true, + false, + ); + } } diff --git a/libs/common/src/auth/abstractions/account-api.service.ts b/libs/common/src/auth/abstractions/account-api.service.ts index 78fbb2cf882..61fdd4f9d68 100644 --- a/libs/common/src/auth/abstractions/account-api.service.ts +++ b/libs/common/src/auth/abstractions/account-api.service.ts @@ -1,6 +1,7 @@ import { RegisterFinishRequest } from "../models/request/registration/register-finish.request"; import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request"; import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request"; +import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request"; import { Verification } from "../types/verification"; export abstract class AccountApiService { @@ -18,7 +19,7 @@ export abstract class AccountApiService { * * @param request - The request object containing * information needed to send the verification email, such as the user's email address. - * @returns A promise that resolves to a string tokencontaining the user's encrypted + * @returns A promise that resolves to a string token containing the user's encrypted * information which must be submitted to complete registration or `null` if * email verification is enabled (users must get the token by clicking a * link in the email that will be sent to them). @@ -33,7 +34,7 @@ export abstract class AccountApiService { * * @param request - The request object containing the email verification token and the * user's email address (which is required to validate the token) - * @returns A promise that resolves when the event is logged on the server succcessfully or a bad + * @returns A promise that resolves when the event is logged on the server successfully or a bad * request if the token is invalid for any reason. */ abstract registerVerificationEmailClicked( @@ -50,4 +51,15 @@ export abstract class AccountApiService { * registration process is successfully completed. */ abstract registerFinish(request: RegisterFinishRequest): Promise; + + /** + * Sets the [dbo].[User].[VerifyDevices] flag to true or false. + * + * @param request - The request object is a SecretVerificationRequest extension + * that also contains the boolean value that the VerifyDevices property is being + * set to. + * @returns A promise that resolves when the process is successfully completed or + * a bad request if secret verification fails. + */ + abstract setVerifyDevices(request: SetVerifyDevicesRequest): Promise; } diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index 094e005e656..1686eefda06 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -43,6 +43,8 @@ export abstract class AccountService { * Observable of the last activity time for each account. */ accountActivity$: Observable>; + /** Observable of the new device login verification property for the account. */ + accountVerifyNewDeviceLogin$: Observable; /** Account list in order of descending recency */ sortedUserIds$: Observable; /** Next account that is not the current active account */ @@ -73,6 +75,15 @@ export abstract class AccountService { * @param emailVerified */ abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise; + /** + * updates the `accounts$` observable with the new VerifyNewDeviceLogin property for the account. + * @param userId + * @param VerifyNewDeviceLogin + */ + abstract setAccountVerifyNewDeviceLogin( + userId: UserId, + verifyNewDeviceLogin: boolean, + ): Promise; /** * Updates the `activeAccount$` observable with the new active account. * @param userId diff --git a/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts index 13963b03bea..24a5d4e8413 100644 --- a/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts @@ -9,7 +9,18 @@ import { DeviceKey, UserKey } from "../../types/key"; import { DeviceResponse } from "./devices/responses/device.response"; export abstract class DeviceTrustServiceAbstraction { + /** + * @deprecated - use supportsDeviceTrustByUserId instead as active user state is being deprecated + * by Platform + * @description Checks if the device trust feature is supported for the active user. + */ supportsDeviceTrust$: Observable; + + /** + * @description Checks if the device trust feature is supported for the given user. + */ + supportsDeviceTrustByUserId$: (userId: UserId) => Observable; + /** * @description Retrieves the users choice to trust the device which can only happen after decryption * Note: this value should only be used once and then reset diff --git a/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts b/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts index 3f3731e0e1b..bf64dcafd69 100644 --- a/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { UserId } from "@bitwarden/common/types/guid"; + export abstract class SsoLoginServiceAbstraction { /** * Gets the code verifier used for SSO. @@ -74,12 +76,16 @@ export abstract class SsoLoginServiceAbstraction { * Gets the value of the active user's organization sso identifier. * * This should only be used post successful SSO login once the user is initialized. + * @param userId The user id for retrieving the org identifier state. */ - getActiveUserOrganizationSsoIdentifier: () => Promise; + getActiveUserOrganizationSsoIdentifier: (userId: UserId) => Promise; /** * Sets the value of the active user's organization sso identifier. * * This should only be used post successful SSO login once the user is initialized. */ - setActiveUserOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise; + setActiveUserOrganizationSsoIdentifier: ( + organizationIdentifier: string, + userId: UserId | undefined, + ) => Promise; } diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 1c176c2b84b..fdc8c963a1b 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -22,6 +22,7 @@ export class AuthResult { ssoEmail2FaSessionToken?: string; email: string; requiresEncryptionKeyMigration: boolean; + requiresDeviceVerification: boolean; get requiresCaptcha() { return !Utils.isNullOrWhitespace(this.captchaSiteKey); diff --git a/libs/common/src/auth/models/request/identity-token/password-token.request.ts b/libs/common/src/auth/models/request/identity-token/password-token.request.ts index 456e058a234..3fe466e143b 100644 --- a/libs/common/src/auth/models/request/identity-token/password-token.request.ts +++ b/libs/common/src/auth/models/request/identity-token/password-token.request.ts @@ -13,6 +13,7 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect public captchaResponse: string, protected twoFactor: TokenTwoFactorRequest, device?: DeviceRequest, + public newDeviceOtp?: string, ) { super(twoFactor, device); } @@ -28,6 +29,10 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect obj.captchaResponse = this.captchaResponse; } + if (this.newDeviceOtp) { + obj.newDeviceOtp = this.newDeviceOtp; + } + return obj; } diff --git a/libs/common/src/auth/models/request/set-verify-devices.request.ts b/libs/common/src/auth/models/request/set-verify-devices.request.ts new file mode 100644 index 00000000000..4835e9f09cc --- /dev/null +++ b/libs/common/src/auth/models/request/set-verify-devices.request.ts @@ -0,0 +1,8 @@ +import { SecretVerificationRequest } from "./secret-verification.request"; + +export class SetVerifyDevicesRequest extends SecretVerificationRequest { + /** + * This is the input for a user update that controls [dbo].[Users].[VerifyDevices] + */ + verifyDevices!: boolean; +} diff --git a/libs/common/src/auth/models/response/identity-device-verification.response.ts b/libs/common/src/auth/models/response/identity-device-verification.response.ts new file mode 100644 index 00000000000..b45f47e99e1 --- /dev/null +++ b/libs/common/src/auth/models/response/identity-device-verification.response.ts @@ -0,0 +1,13 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class IdentityDeviceVerificationResponse extends BaseResponse { + deviceVerified: boolean; + captchaToken: string; + + constructor(response: any) { + super(response); + this.deviceVerified = this.getResponseProperty("DeviceVerified") ?? false; + + this.captchaToken = this.getResponseProperty("CaptchaBypassToken"); + } +} diff --git a/libs/common/src/auth/models/response/identity-response.ts b/libs/common/src/auth/models/response/identity-response.ts new file mode 100644 index 00000000000..26503a9cc2f --- /dev/null +++ b/libs/common/src/auth/models/response/identity-response.ts @@ -0,0 +1,8 @@ +import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; +import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; +import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; + +export type IdentityResponse = + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityDeviceVerificationResponse; diff --git a/libs/common/src/auth/services/account-api.service.ts b/libs/common/src/auth/services/account-api.service.ts index e10b0686f61..0347694c465 100644 --- a/libs/common/src/auth/services/account-api.service.ts +++ b/libs/common/src/auth/services/account-api.service.ts @@ -10,6 +10,7 @@ import { UserVerificationService } from "../abstractions/user-verification/user- import { RegisterFinishRequest } from "../models/request/registration/register-finish.request"; import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request"; import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request"; +import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request"; import { Verification } from "../types/verification"; export class AccountApiServiceImplementation implements AccountApiService { @@ -102,4 +103,21 @@ export class AccountApiServiceImplementation implements AccountApiService { throw e; } } + + async setVerifyDevices(request: SetVerifyDevicesRequest): Promise { + try { + const response = await this.apiService.send( + "POST", + "/accounts/verify-devices", + request, + true, + true, + ); + + return response; + } catch (e: unknown) { + this.logService.error(e); + throw e; + } + } } diff --git a/libs/common/src/auth/services/account.service.spec.ts b/libs/common/src/auth/services/account.service.spec.ts index 227949156ee..3fc47002083 100644 --- a/libs/common/src/auth/services/account.service.spec.ts +++ b/libs/common/src/auth/services/account.service.spec.ts @@ -7,7 +7,10 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom } from "rxjs"; import { FakeGlobalState } from "../../../spec/fake-state"; -import { FakeGlobalStateProvider } from "../../../spec/fake-state-provider"; +import { + FakeGlobalStateProvider, + FakeSingleUserStateProvider, +} from "../../../spec/fake-state-provider"; import { trackEmissions } from "../../../spec/utils"; import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; @@ -19,6 +22,7 @@ import { ACCOUNT_ACCOUNTS, ACCOUNT_ACTIVE_ACCOUNT_ID, ACCOUNT_ACTIVITY, + ACCOUNT_VERIFY_NEW_DEVICE_LOGIN, AccountServiceImplementation, } from "./account.service"; @@ -66,9 +70,11 @@ describe("accountService", () => { let messagingService: MockProxy; let logService: MockProxy; let globalStateProvider: FakeGlobalStateProvider; + let singleUserStateProvider: FakeSingleUserStateProvider; let sut: AccountServiceImplementation; let accountsState: FakeGlobalState>; let activeAccountIdState: FakeGlobalState; + let accountActivityState: FakeGlobalState>; const userId = Utils.newGuid() as UserId; const userInfo = { email: "email", name: "name", emailVerified: true }; @@ -76,11 +82,18 @@ describe("accountService", () => { messagingService = mock(); logService = mock(); globalStateProvider = new FakeGlobalStateProvider(); + singleUserStateProvider = new FakeSingleUserStateProvider(); - sut = new AccountServiceImplementation(messagingService, logService, globalStateProvider); + sut = new AccountServiceImplementation( + messagingService, + logService, + globalStateProvider, + singleUserStateProvider, + ); accountsState = globalStateProvider.getFake(ACCOUNT_ACCOUNTS); activeAccountIdState = globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID); + accountActivityState = globalStateProvider.getFake(ACCOUNT_ACTIVITY); }); afterEach(() => { @@ -126,6 +139,22 @@ describe("accountService", () => { }); }); + describe("accountsVerifyNewDeviceLogin$", () => { + it("returns expected value", async () => { + // Arrange + const expected = true; + // we need to set this state since it is how we initialize the VerifyNewDeviceLogin$ + activeAccountIdState.stateSubject.next(userId); + singleUserStateProvider.getFake(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN).nextState(expected); + + // Act + const result = await firstValueFrom(sut.accountVerifyNewDeviceLogin$); + + // Assert + expect(result).toEqual(expected); + }); + }); + describe("addAccount", () => { it("should emit the new account", async () => { await sut.addAccount(userId, userInfo); @@ -224,6 +253,33 @@ describe("accountService", () => { }); }); + describe("setAccountVerifyNewDeviceLogin", () => { + const initialState = true; + beforeEach(() => { + activeAccountIdState.stateSubject.next(userId); + singleUserStateProvider + .getFake(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN) + .nextState(initialState); + }); + + it("should update the VerifyNewDeviceLogin", async () => { + const expected = false; + expect(await firstValueFrom(sut.accountVerifyNewDeviceLogin$)).toEqual(initialState); + + await sut.setAccountVerifyNewDeviceLogin(userId, expected); + const currentState = await firstValueFrom(sut.accountVerifyNewDeviceLogin$); + + expect(currentState).toEqual(expected); + }); + + it("should NOT update VerifyNewDeviceLogin when userId is null", async () => { + await sut.setAccountVerifyNewDeviceLogin(null, false); + const currentState = await firstValueFrom(sut.accountVerifyNewDeviceLogin$); + + expect(currentState).toEqual(initialState); + }); + }); + describe("clean", () => { beforeEach(() => { accountsState.stateSubject.next({ [userId]: userInfo }); @@ -256,6 +312,7 @@ describe("accountService", () => { beforeEach(() => { accountsState.stateSubject.next({ [userId]: userInfo }); activeAccountIdState.stateSubject.next(userId); + accountActivityState.stateSubject.next({ [userId]: new Date(1) }); }); it("should emit null if no account is provided", async () => { @@ -269,6 +326,34 @@ describe("accountService", () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist"); }); + + it("should change active account when switched to the new account", async () => { + const newUserId = Utils.newGuid() as UserId; + accountsState.stateSubject.next({ [newUserId]: userInfo }); + + await sut.switchAccount(newUserId); + + await expect(firstValueFrom(sut.activeAccount$)).resolves.toEqual({ + id: newUserId, + ...userInfo, + }); + await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({ + [userId]: new Date(1), + [newUserId]: expect.toAlmostEqual(new Date(), 1000), + }); + }); + + it("should not change active account when already switched to the same account", async () => { + await sut.switchAccount(userId); + + await expect(firstValueFrom(sut.activeAccount$)).resolves.toEqual({ + id: userId, + ...userInfo, + }); + await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({ + [userId]: new Date(1), + }); + }); }); describe("account activity", () => { diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index d4479815c5d..50ba2455d78 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -7,6 +7,10 @@ import { shareReplay, combineLatest, Observable, + switchMap, + filter, + timeout, + of, } from "rxjs"; import { @@ -23,6 +27,8 @@ import { GlobalState, GlobalStateProvider, KeyDefinition, + SingleUserStateProvider, + UserKeyDefinition, } from "../../platform/state"; import { UserId } from "../../types/guid"; @@ -42,6 +48,15 @@ export const ACCOUNT_ACTIVITY = KeyDefinition.record(ACCOUNT_DISK, deserializer: (activity) => new Date(activity), }); +export const ACCOUNT_VERIFY_NEW_DEVICE_LOGIN = new UserKeyDefinition( + ACCOUNT_DISK, + "verifyNewDeviceLogin", + { + deserializer: (verifyDevices) => verifyDevices, + clearOn: ["logout"], + }, +); + const LOGGED_OUT_INFO: AccountInfo = { email: "", emailVerified: false, @@ -73,6 +88,7 @@ export class AccountServiceImplementation implements InternalAccountService { accounts$: Observable>; activeAccount$: Observable; accountActivity$: Observable>; + accountVerifyNewDeviceLogin$: Observable; sortedUserIds$: Observable; nextUpAccount$: Observable; @@ -80,6 +96,7 @@ export class AccountServiceImplementation implements InternalAccountService { private messagingService: MessagingService, private logService: LogService, private globalStateProvider: GlobalStateProvider, + private singleUserStateProvider: SingleUserStateProvider, ) { this.accountsState = this.globalStateProvider.get(ACCOUNT_ACCOUNTS); this.activeAccountIdState = this.globalStateProvider.get(ACCOUNT_ACTIVE_ACCOUNT_ID); @@ -114,6 +131,12 @@ export class AccountServiceImplementation implements InternalAccountService { return nextId ? { id: nextId, ...accounts[nextId] } : null; }), ); + this.accountVerifyNewDeviceLogin$ = this.activeAccountIdState.state$.pipe( + switchMap( + (userId) => + this.singleUserStateProvider.get(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN).state$, + ), + ); } async addAccount(userId: UserId, accountData: AccountInfo): Promise { @@ -149,21 +172,28 @@ export class AccountServiceImplementation implements InternalAccountService { async switchAccount(userId: UserId | null): Promise { let updateActivity = false; await this.activeAccountIdState.update( - (_, accounts) => { - if (userId == null) { - // indicates no account is active - return null; - } - - if (accounts?.[userId] == null) { - throw new Error("Account does not exist"); - } + (_, __) => { updateActivity = true; return userId; }, { - combineLatestWith: this.accounts$, - shouldUpdate: (id) => { + combineLatestWith: this.accountsState.state$.pipe( + filter((accounts) => { + if (userId == null) { + // Don't worry about accounts when we are about to set active user to null + return true; + } + + return accounts?.[userId] != null; + }), + // If we don't get the desired account with enough time, just return empty as that will result in the same error + timeout({ first: 1000, with: () => of({} as Record) }), + ), + shouldUpdate: (id, accounts) => { + if (userId != null && accounts?.[userId] == null) { + throw new Error("Account does not exist"); + } + // update only if userId changes return id !== userId; }, @@ -193,6 +223,20 @@ export class AccountServiceImplementation implements InternalAccountService { ); } + async setAccountVerifyNewDeviceLogin( + userId: UserId, + setVerifyNewDeviceLogin: boolean, + ): Promise { + if (!Utils.isGuid(userId)) { + // only store for valid userIds + return; + } + + await this.singleUserStateProvider.get(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN).update(() => { + return setVerifyNewDeviceLogin; + }); + } + async removeAccountActivity(userId: UserId): Promise { await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update( (activity) => { diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index a94c8b6e422..903c72d4211 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -7,10 +7,10 @@ import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common" // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; @@ -81,7 +81,17 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { private configService: ConfigService, ) { this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( - map((options) => options?.trustedDeviceOption != null ?? false), + map((options) => { + return options?.trustedDeviceOption != null ?? false; + }), + ); + } + + supportsDeviceTrustByUserId$(userId: UserId): Observable { + return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe( + map((options) => { + return options?.trustedDeviceOption != null ?? false; + }), ); } diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index 943653e3129..06da1396074 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -1,5 +1,7 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { matches, mock } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; @@ -13,10 +15,10 @@ import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-a import { FakeActiveUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { DeviceType } from "../../enums"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; @@ -74,17 +76,56 @@ describe("deviceTrustService", () => { userId: mockUserId, }; + let userDecryptionOptions: UserDecryptionOptions; + beforeEach(() => { jest.clearAllMocks(); const supportsSecureStorage = false; // default to false; tests will override as needed // By default all the tests will have a mocked active user in state provider. deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); + + userDecryptionOptions = new UserDecryptionOptions(); }); it("instantiates", () => { expect(deviceTrustService).not.toBeFalsy(); }); + describe("supportsDeviceTrustByUserId$", () => { + it("returns true when the user has a non-null trusted device decryption option", async () => { + // Arrange + userDecryptionOptions.trustedDeviceOption = { + hasAdminApproval: false, + hasLoginApprovingDevice: false, + hasManageResetPasswordPermission: false, + isTdeOffboarding: false, + }; + + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + new BehaviorSubject(userDecryptionOptions), + ); + + const result = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(mockUserId), + ); + expect(result).toBe(true); + }); + + it("returns false when the user has a null trusted device decryption option", async () => { + // Arrange + userDecryptionOptions.trustedDeviceOption = null; + + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + new BehaviorSubject(userDecryptionOptions), + ); + + const result = await firstValueFrom( + deviceTrustService.supportsDeviceTrustByUserId$(mockUserId), + ); + expect(result).toBe(false); + }); + }); + describe("User Trust Device Choice For Decryption", () => { describe("getShouldTrustDevice", () => { it("gets the user trust device choice for decryption", async () => { diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index 660f1124f4a..165dcee1ea8 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -1,11 +1,13 @@ import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; import { ApiService } from "../../abstractions/api.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "../../admin-console/models/data/organization.data"; import { Organization } from "../../admin-console/models/domain/organization"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; @@ -95,7 +97,7 @@ describe("KeyConnectorService", () => { organizationData(true, false, "https://key-connector-url.com", 2, false), organizationData(true, true, "https://other-url.com", 2, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -110,7 +112,7 @@ describe("KeyConnectorService", () => { organizationData(true, false, "https://key-connector-url.com", 2, false), organizationData(false, false, "https://key-connector-url.com", 2, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -125,7 +127,7 @@ describe("KeyConnectorService", () => { organizationData(true, true, "https://key-connector-url.com", 0, false), organizationData(true, true, "https://key-connector-url.com", 1, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -140,7 +142,7 @@ describe("KeyConnectorService", () => { organizationData(true, true, "https://key-connector-url.com", 2, true), organizationData(false, true, "https://key-connector-url.com", 2, true), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -181,7 +183,7 @@ describe("KeyConnectorService", () => { // create organization object const data = organizationData(true, true, "https://key-connector-url.com", 2, false); - organizationService.getAll.mockResolvedValue([data]); + organizationService.organizations$.mockReturnValue(of([data])); // uses KeyConnector const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); @@ -195,7 +197,7 @@ describe("KeyConnectorService", () => { it("should return false if the user does not need migration", async () => { tokenService.getIsExternal.mockResolvedValue(false); const data = organizationData(false, false, "https://key-connector-url.com", 2, false); - organizationService.getAll.mockResolvedValue([data]); + organizationService.organizations$.mockReturnValue(of([data])); const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); state.nextState(true); @@ -275,7 +277,7 @@ describe("KeyConnectorService", () => { const masterKey = getMockMasterKey(); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); const error = new Error("Failed to post user key to key connector"); - organizationService.getAll.mockResolvedValue([organization]); + organizationService.organizations$.mockReturnValue(of([organization])); masterPasswordService.masterKeySubject.next(masterKey); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); @@ -366,6 +368,7 @@ describe("KeyConnectorService", () => { accessSecretsManager: false, limitCollectionCreation: true, limitCollectionDeletion: true, + limitItemDeletion: true, allowAdminAccessToAllCollectionItems: true, flexibleCollections: false, object: "profileOrganization", diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index f798413162e..f6f76579ee5 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -3,6 +3,8 @@ import { firstValueFrom } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Argon2KdfConfig, KdfConfig, @@ -12,7 +14,6 @@ import { } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "../../admin-console/enums"; import { Organization } from "../../admin-console/models/domain/organization"; import { KeysRequest } from "../../models/request/keys.request"; @@ -28,7 +29,6 @@ import { } from "../../platform/state"; import { UserId } from "../../types/guid"; import { MasterKey } from "../../types/key"; -import { AccountService } from "../abstractions/account.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; import { TokenService } from "../abstractions/token.service"; @@ -122,7 +122,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } async getManagingOrganization(userId?: UserId): Promise { - const orgs = await this.organizationService.getAll(userId); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); return orgs.find( (o) => o.keyConnectorEnabled && diff --git a/libs/common/src/auth/services/master-password/master-password.service.ts b/libs/common/src/auth/services/master-password/master-password.service.ts index 14e7522a836..9b5ce588bd3 100644 --- a/libs/common/src/auth/services/master-password/master-password.service.ts +++ b/libs/common/src/auth/services/master-password/master-password.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { firstValueFrom, map, Observable } from "rxjs"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { LogService } from "../../../platform/abstractions/log.service"; import { StateService } from "../../../platform/abstractions/state.service"; diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index ddd24ae7907..8953d14c8e6 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -8,7 +8,7 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { UserId } from "../../types/guid"; import { Account, AccountInfo, AccountService } from "../abstractions/account.service"; diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts index 22d5384e6ac..c0a961d5bbb 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts @@ -11,7 +11,7 @@ import { // eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { Utils } from "../../platform/misc/utils"; import { UserKey } from "../../types/key"; diff --git a/libs/common/src/auth/services/sso-login.service.spec.ts b/libs/common/src/auth/services/sso-login.service.spec.ts new file mode 100644 index 00000000000..9cf49a07834 --- /dev/null +++ b/libs/common/src/auth/services/sso-login.service.spec.ts @@ -0,0 +1,94 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { + CODE_VERIFIER, + GLOBAL_ORGANIZATION_SSO_IDENTIFIER, + SSO_EMAIL, + SSO_STATE, + SsoLoginService, + USER_ORGANIZATION_SSO_IDENTIFIER, +} from "@bitwarden/common/auth/services/sso-login.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; + +describe("SSOLoginService ", () => { + let sut: SsoLoginService; + + let accountService: FakeAccountService; + let mockSingleUserStateProvider: FakeStateProvider; + let mockLogService: MockProxy; + let userId: UserId; + + beforeEach(() => { + jest.clearAllMocks(); + + userId = Utils.newGuid() as UserId; + accountService = mockAccountServiceWith(userId); + mockSingleUserStateProvider = new FakeStateProvider(accountService); + mockLogService = mock(); + + sut = new SsoLoginService(mockSingleUserStateProvider, mockLogService); + }); + + it("instantiates", () => { + expect(sut).not.toBeFalsy(); + }); + + it("gets and sets code verifier", async () => { + const codeVerifier = "test-code-verifier"; + await sut.setCodeVerifier(codeVerifier); + mockSingleUserStateProvider.getGlobal(CODE_VERIFIER); + + const result = await sut.getCodeVerifier(); + expect(result).toBe(codeVerifier); + }); + + it("gets and sets SSO state", async () => { + const ssoState = "test-sso-state"; + await sut.setSsoState(ssoState); + mockSingleUserStateProvider.getGlobal(SSO_STATE); + + const result = await sut.getSsoState(); + expect(result).toBe(ssoState); + }); + + it("gets and sets organization SSO identifier", async () => { + const orgIdentifier = "test-org-identifier"; + await sut.setOrganizationSsoIdentifier(orgIdentifier); + mockSingleUserStateProvider.getGlobal(GLOBAL_ORGANIZATION_SSO_IDENTIFIER); + + const result = await sut.getOrganizationSsoIdentifier(); + expect(result).toBe(orgIdentifier); + }); + + it("gets and sets SSO email", async () => { + const email = "test@example.com"; + await sut.setSsoEmail(email); + mockSingleUserStateProvider.getGlobal(SSO_EMAIL); + + const result = await sut.getSsoEmail(); + expect(result).toBe(email); + }); + + it("gets and sets active user organization SSO identifier", async () => { + const userId = Utils.newGuid() as UserId; + const orgIdentifier = "test-active-org-identifier"; + await sut.setActiveUserOrganizationSsoIdentifier(orgIdentifier, userId); + mockSingleUserStateProvider.getUser(userId, USER_ORGANIZATION_SSO_IDENTIFIER); + + const result = await sut.getActiveUserOrganizationSsoIdentifier(userId); + expect(result).toBe(orgIdentifier); + }); + + it("logs error when setting active user organization SSO identifier with undefined userId", async () => { + const orgIdentifier = "test-active-org-identifier"; + await sut.setActiveUserOrganizationSsoIdentifier(orgIdentifier, undefined); + + expect(mockLogService.warning).toHaveBeenCalledWith( + "Tried to set a user organization sso identifier with an undefined user id.", + ); + }); +}); diff --git a/libs/common/src/auth/services/sso-login.service.ts b/libs/common/src/auth/services/sso-login.service.ts index 32019e8d568..c73be3630be 100644 --- a/libs/common/src/auth/services/sso-login.service.ts +++ b/libs/common/src/auth/services/sso-login.service.ts @@ -2,10 +2,13 @@ // @ts-strict-ignore import { firstValueFrom } from "rxjs"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; + import { - ActiveUserState, GlobalState, KeyDefinition, + SingleUserState, SSO_DISK, StateProvider, UserKeyDefinition, @@ -15,21 +18,21 @@ import { SsoLoginServiceAbstraction } from "../abstractions/sso-login.service.ab /** * Uses disk storage so that the code verifier can be persisted across sso redirects. */ -const CODE_VERIFIER = new KeyDefinition(SSO_DISK, "ssoCodeVerifier", { +export const CODE_VERIFIER = new KeyDefinition(SSO_DISK, "ssoCodeVerifier", { deserializer: (codeVerifier) => codeVerifier, }); /** * Uses disk storage so that the sso state can be persisted across sso redirects. */ -const SSO_STATE = new KeyDefinition(SSO_DISK, "ssoState", { +export const SSO_STATE = new KeyDefinition(SSO_DISK, "ssoState", { deserializer: (state) => state, }); /** * Uses disk storage so that the organization sso identifier can be persisted across sso redirects. */ -const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition( +export const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition( SSO_DISK, "organizationSsoIdentifier", { @@ -41,7 +44,7 @@ const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition( /** * Uses disk storage so that the organization sso identifier can be persisted across sso redirects. */ -const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition( +export const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition( SSO_DISK, "organizationSsoIdentifier", { @@ -52,7 +55,7 @@ const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition( /** * Uses disk storage so that the user's email can be persisted across sso redirects. */ -const SSO_EMAIL = new KeyDefinition(SSO_DISK, "ssoEmail", { +export const SSO_EMAIL = new KeyDefinition(SSO_DISK, "ssoEmail", { deserializer: (state) => state, }); @@ -61,16 +64,15 @@ export class SsoLoginService implements SsoLoginServiceAbstraction { private ssoState: GlobalState; private orgSsoIdentifierState: GlobalState; private ssoEmailState: GlobalState; - private activeUserOrgSsoIdentifierState: ActiveUserState; - constructor(private stateProvider: StateProvider) { + constructor( + private stateProvider: StateProvider, + private logService: LogService, + ) { this.codeVerifierState = this.stateProvider.getGlobal(CODE_VERIFIER); this.ssoState = this.stateProvider.getGlobal(SSO_STATE); this.orgSsoIdentifierState = this.stateProvider.getGlobal(GLOBAL_ORGANIZATION_SSO_IDENTIFIER); this.ssoEmailState = this.stateProvider.getGlobal(SSO_EMAIL); - this.activeUserOrgSsoIdentifierState = this.stateProvider.getActive( - USER_ORGANIZATION_SSO_IDENTIFIER, - ); } getCodeVerifier(): Promise { @@ -105,11 +107,24 @@ export class SsoLoginService implements SsoLoginServiceAbstraction { await this.ssoEmailState.update((_) => email); } - getActiveUserOrganizationSsoIdentifier(): Promise { - return firstValueFrom(this.activeUserOrgSsoIdentifierState.state$); + getActiveUserOrganizationSsoIdentifier(userId: UserId): Promise { + return firstValueFrom(this.userOrgSsoIdentifierState(userId).state$); } - async setActiveUserOrganizationSsoIdentifier(organizationIdentifier: string): Promise { - await this.activeUserOrgSsoIdentifierState.update((_) => organizationIdentifier); + async setActiveUserOrganizationSsoIdentifier( + organizationIdentifier: string, + userId: UserId | undefined, + ): Promise { + if (userId === undefined) { + this.logService.warning( + "Tried to set a user organization sso identifier with an undefined user id.", + ); + return; + } + await this.userOrgSsoIdentifierState(userId).update((_) => organizationIdentifier); + } + + private userOrgSsoIdentifierState(userId: UserId): SingleUserState { + return this.stateProvider.getUser(userId, USER_ORGANIZATION_SSO_IDENTIFIER); } } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index f8882e1b118..339f570a003 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -5,7 +5,7 @@ import { LogoutReason } from "@bitwarden/auth/common"; import { FakeSingleUserStateProvider, FakeGlobalStateProvider } from "../../../spec"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 4b7cc2cab01..72e082f2002 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -6,7 +6,7 @@ import { Opaque } from "type-fest"; import { LogoutReason, decodeJwtTokenToJson } from "@bitwarden/auth/common"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 1e68488ac98..69309014fac 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -4,7 +4,6 @@ import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; -import { BillingSourceResponse } from "../models/response/billing.response"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; export type OrganizationInformation = { @@ -46,9 +45,7 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - getPaymentSource: ( - organizationId: string, - ) => Promise; + getPaymentSource: (organizationId: string) => Promise; purchaseSubscription: (subscription: SubscriptionInformation) => Promise; diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index da1a1666ff0..83efbf0a30c 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -7,9 +7,7 @@ import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../ import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; -import { FeatureFlag } from "../../enums/feature-flag.enum"; -import { ConfigService } from "../../platform/abstractions/config/config.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { SyncService } from "../../platform/sync"; @@ -24,7 +22,6 @@ import { } from "../abstractions"; import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; -import { BillingSourceResponse } from "../models/response/billing.response"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; interface OrganizationKeys { @@ -38,7 +35,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs constructor( private apiService: ApiService, private billingApiService: BillingApiServiceAbstraction, - private configService: ConfigService, private keyService: KeyService, private encryptService: EncryptService, private i18nService: I18nService, @@ -46,21 +42,9 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} - async getPaymentSource( - organizationId: string, - ): Promise { - const deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( - FeatureFlag.AC2476_DeprecateStripeSourcesAPI, - ); - - if (deprecateStripeSourcesAPI) { - const paymentMethod = - await this.billingApiService.getOrganizationPaymentMethod(organizationId); - return paymentMethod.paymentSource; - } else { - const billing = await this.organizationApiService.getBilling(organizationId); - return billing.paymentSource; - } + async getPaymentSource(organizationId: string): Promise { + const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId); + return paymentMethod.paymentSource; } async purchaseSubscription(subscription: SubscriptionInformation): Promise { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 84f28e1890d..550a8c07ff7 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -9,6 +9,7 @@ export enum FeatureFlag { AccountDeprovisioning = "pm-10308-account-deprovisioning", VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", PM14505AdminConsoleIntegrationPage = "pm-14505-admin-console-integration-page", + LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission", /* Autofill */ BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", @@ -23,8 +24,12 @@ export enum FeatureFlag { NotificationRefresh = "notification-refresh", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", + /* Tools */ ItemShare = "item-share", GeneratorToolsModernization = "generator-tools-modernization", + CriticalApps = "pm-14466-risk-insights-critical-application", + EnableRiskInsightsNotifications = "enable-risk-insights-notifications", + AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", ExtensionRefresh = "extension-refresh", PersistPopupView = "persist-popup-view", @@ -34,18 +39,19 @@ export enum FeatureFlag { UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", SSHKeyVaultItem = "ssh-key-vault-item", SSHAgent = "ssh-agent", - AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api", CipherKeyEncryption = "cipher-key-encryption", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", - CriticalApps = "pm-14466-risk-insights-critical-application", TrialPaymentOptional = "PM-8163-trial-payment", SecurityTasks = "security-tasks", NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", - DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", MacOsNativeCredentialSync = "macos-native-credential-sync", + PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", PrivateKeyRegeneration = "pm-12241-private-key-regeneration", ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs", + AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner", + NewDeviceVerification = "new-device-verification", + PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -65,6 +71,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.AccountDeprovisioning]: FALSE, [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, [FeatureFlag.PM14505AdminConsoleIntegrationPage]: FALSE, + [FeatureFlag.LimitItemDeletion]: FALSE, /* Autofill */ [FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE, @@ -79,8 +86,12 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.NotificationRefresh]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, + /* Tools */ [FeatureFlag.ItemShare]: FALSE, [FeatureFlag.GeneratorToolsModernization]: FALSE, + [FeatureFlag.CriticalApps]: FALSE, + [FeatureFlag.EnableRiskInsightsNotifications]: FALSE, + [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.PersistPopupView]: FALSE, @@ -90,18 +101,19 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, [FeatureFlag.SSHKeyVaultItem]: FALSE, [FeatureFlag.SSHAgent]: FALSE, - [FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, - [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, - [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, + [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.ResellerManagedOrgAlert]: FALSE, + [FeatureFlag.AccountDeprovisioningBanner]: FALSE, + [FeatureFlag.NewDeviceVerification]: FALSE, + [FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/enums/push-technology.enum.ts b/libs/common/src/enums/push-technology.enum.ts new file mode 100644 index 00000000000..9452c144bb7 --- /dev/null +++ b/libs/common/src/enums/push-technology.enum.ts @@ -0,0 +1,13 @@ +/** + * The preferred push technology of the server. + */ +export enum PushTechnology { + /** + * Indicates that we should use SignalR over web sockets to receive push notifications from the server. + */ + SignalR = 0, + /** + * Indicatates that we should use WebPush to receive push notifications from the server. + */ + WebPush = 1, +} diff --git a/libs/common/src/key-management/crypto/abstractions/bulk-encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/bulk-encrypt.service.ts new file mode 100644 index 00000000000..3e47ccdb5f2 --- /dev/null +++ b/libs/common/src/key-management/crypto/abstractions/bulk-encrypt.service.ts @@ -0,0 +1,10 @@ +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +export abstract class BulkEncryptService { + abstract decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise; +} diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts similarity index 82% rename from libs/common/src/platform/abstractions/encrypt.service.ts rename to libs/common/src/key-management/crypto/abstractions/encrypt.service.ts index a660524699d..e00d053ce7b 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -1,9 +1,9 @@ -import { Decryptable } from "../interfaces/decryptable.interface"; -import { Encrypted } from "../interfaces/encrypted"; -import { InitializerMetadata } from "../interfaces/initializer-metadata.interface"; -import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; -import { EncString } from "../models/domain/enc-string"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; export abstract class EncryptService { abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise; diff --git a/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts similarity index 84% rename from libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts rename to libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts index 1320fbae0e0..1d1e0f52279 100644 --- a/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts @@ -3,15 +3,14 @@ import { firstValueFrom, fromEvent, filter, map, takeUntil, defaultIfEmpty, Subject } from "rxjs"; import { Jsonify } from "type-fest"; -import { BulkEncryptService } from "../../abstractions/bulk-encrypt.service"; -import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; -import { LogService } from "../../abstractions/log.service"; -import { Decryptable } from "../../interfaces/decryptable.interface"; -import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; -import { Utils } from "../../misc/utils"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; - -import { getClassInitializer } from "./get-class-initializer"; +import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer"; // TTL (time to live) is not strictly required but avoids tying up memory resources if inactive const workerTTL = 60000; // 1 minute @@ -88,7 +87,7 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService { new Worker( new URL( /* webpackChunkName: 'encrypt-worker' */ - "@bitwarden/common/platform/services/cryptography/encrypt.worker.ts", + "@bitwarden/common/key-management/crypto/services/encrypt.worker.ts", import.meta.url, ), ), diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts similarity index 90% rename from libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts rename to libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index 68263cadf27..075b9da4964 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -1,17 +1,21 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Utils } from "../../../platform/misc/utils"; -import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; -import { EncryptService } from "../../abstractions/encrypt.service"; -import { LogService } from "../../abstractions/log.service"; -import { EncryptionType, encryptionTypeToString as encryptionTypeName } from "../../enums"; -import { Decryptable } from "../../interfaces/decryptable.interface"; -import { Encrypted } from "../../interfaces/encrypted"; -import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; -import { EncArrayBuffer } from "../../models/domain/enc-array-buffer"; -import { EncString } from "../../models/domain/enc-string"; -import { EncryptedObject } from "../../models/domain/encrypted-object"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + EncryptionType, + encryptionTypeToString as encryptionTypeName, +} from "@bitwarden/common/platform/enums"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +import { EncryptService } from "../abstractions/encrypt.service"; export class EncryptServiceImplementation implements EncryptService { constructor( diff --git a/libs/common/src/platform/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts similarity index 91% rename from libs/common/src/platform/services/encrypt.service.spec.ts rename to libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 609b5100a10..8d75b528596 100644 --- a/libs/common/src/platform/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -1,15 +1,17 @@ import { mockReset, mock } from "jest-mock-extended"; -import { makeStaticByteArray } from "../../../spec"; -import { CsprngArray } from "../../types/csprng"; -import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { LogService } from "../abstractions/log.service"; -import { EncryptionType } from "../enums"; -import { Utils } from "../misc/utils"; -import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; -import { EncString } from "../models/domain/enc-string"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; -import { EncryptServiceImplementation } from "../services/cryptography/encrypt.service.implementation"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; + +import { makeStaticByteArray } from "../../../../spec"; + +import { EncryptServiceImplementation } from "./encrypt.service.implementation"; describe("EncryptService", () => { const cryptoFunctionService = mock(); diff --git a/libs/common/src/platform/services/cryptography/encrypt.worker.ts b/libs/common/src/key-management/crypto/services/encrypt.worker.ts similarity index 71% rename from libs/common/src/platform/services/cryptography/encrypt.worker.ts rename to libs/common/src/key-management/crypto/services/encrypt.worker.ts index a293e1c6bb0..84ffcf56934 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.worker.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.worker.ts @@ -2,14 +2,14 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { Decryptable } from "../../interfaces/decryptable.interface"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { ConsoleLogService } from "../console-log.service"; -import { ContainerService } from "../container.service"; -import { WebCryptoFunctionService } from "../web-crypto-function.service"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer"; +import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { EncryptServiceImplementation } from "./encrypt.service.implementation"; -import { getClassInitializer } from "./get-class-initializer"; const workerApi: Worker = self as any; diff --git a/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts similarity index 68% rename from libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts rename to libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts index 7a4fd8f3c1d..80fdd27895d 100644 --- a/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts +++ b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts @@ -1,10 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BulkEncryptService } from "../../abstractions/bulk-encrypt.service"; -import { EncryptService } from "../../abstractions/encrypt.service"; -import { Decryptable } from "../../interfaces/decryptable.interface"; -import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; +import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +import { EncryptService } from "../abstractions/encrypt.service"; /** * @deprecated For the feature flag from PM-4154, remove once feature is rolled out diff --git a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts similarity index 81% rename from libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts rename to libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts index 100dcf152e6..0bf96851563 100644 --- a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts @@ -3,13 +3,13 @@ import { defaultIfEmpty, filter, firstValueFrom, fromEvent, map, Subject, takeUntil } from "rxjs"; import { Jsonify } from "type-fest"; -import { Utils } from "../../../platform/misc/utils"; -import { Decryptable } from "../../interfaces/decryptable.interface"; -import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer"; import { EncryptServiceImplementation } from "./encrypt.service.implementation"; -import { getClassInitializer } from "./get-class-initializer"; // TTL (time to live) is not strictly required but avoids tying up memory resources if inactive const workerTTL = 3 * 60000; // 3 minutes @@ -40,7 +40,7 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple this.worker ??= new Worker( new URL( /* webpackChunkName: 'encrypt-worker' */ - "@bitwarden/common/platform/services/cryptography/encrypt.worker.ts", + "@bitwarden/common/key-management/crypto/services/encrypt.worker.ts", import.meta.url, ), ); diff --git a/libs/common/src/models/response/profile.response.ts b/libs/common/src/models/response/profile.response.ts index 6b6555fc566..9aee5acbce8 100644 --- a/libs/common/src/models/response/profile.response.ts +++ b/libs/common/src/models/response/profile.response.ts @@ -21,6 +21,7 @@ export class ProfileResponse extends BaseResponse { securityStamp: string; forcePasswordReset: boolean; usesKeyConnector: boolean; + verifyDevices: boolean; organizations: ProfileOrganizationResponse[] = []; providers: ProfileProviderResponse[] = []; providerOrganizations: ProfileProviderOrganizationResponse[] = []; @@ -42,6 +43,7 @@ export class ProfileResponse extends BaseResponse { this.securityStamp = this.getResponseProperty("SecurityStamp"); this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset") ?? false; this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false; + this.verifyDevices = this.getResponseProperty("VerifyDevices") ?? true; const organizations = this.getResponseProperty("Organizations"); if (organizations != null) { diff --git a/libs/common/src/platform/abstractions/bulk-encrypt.service.ts b/libs/common/src/platform/abstractions/bulk-encrypt.service.ts deleted file mode 100644 index 4cdff0c769a..00000000000 --- a/libs/common/src/platform/abstractions/bulk-encrypt.service.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Decryptable } from "../interfaces/decryptable.interface"; -import { InitializerMetadata } from "../interfaces/initializer-metadata.interface"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; - -export abstract class BulkEncryptService { - abstract decryptItems( - items: Decryptable[], - key: SymmetricCryptoKey, - ): Promise; -} diff --git a/libs/common/src/platform/abstractions/config/server-config.ts b/libs/common/src/platform/abstractions/config/server-config.ts index f77239b3016..8e08cc4e16c 100644 --- a/libs/common/src/platform/abstractions/config/server-config.ts +++ b/libs/common/src/platform/abstractions/config/server-config.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum"; +import { PushTechnology } from "../../../enums/push-technology.enum"; import { ServerConfigData, ThirdPartyServerConfigData, @@ -10,6 +11,11 @@ import { } from "../../models/data/server-config.data"; import { ServerSettings } from "../../models/domain/server-settings"; +type PushConfig = + | { pushTechnology: PushTechnology.SignalR } + | { pushTechnology: PushTechnology.WebPush; vapidPublicKey: string } + | undefined; + const dayInMilliseconds = 24 * 3600 * 1000; export class ServerConfig { @@ -19,6 +25,7 @@ export class ServerConfig { environment?: EnvironmentServerConfigData; utcDate: Date; featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; + push: PushConfig; settings: ServerSettings; constructor(serverConfigData: ServerConfigData) { @@ -28,6 +35,15 @@ export class ServerConfig { this.utcDate = new Date(serverConfigData.utcDate); this.environment = serverConfigData.environment; this.featureStates = serverConfigData.featureStates; + this.push = + serverConfigData.push == null + ? { + pushTechnology: PushTechnology.SignalR, + } + : { + pushTechnology: serverConfigData.push.pushTechnology, + vapidPublicKey: serverConfigData.push.vapidPublicKey, + }; this.settings = serverConfigData.settings; if (this.server?.name == null && this.server?.url == null) { diff --git a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts index d684561dacd..6a1b7b67b42 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts @@ -4,6 +4,10 @@ import type { BitwardenClient } from "@bitwarden/sdk-internal"; * Factory for creating SDK clients. */ export abstract class SdkClientFactory { + /** + * Creates a new BitwardenClient. Assumes the SDK is already loaded. + * @param args Bitwarden client constructor parameters + */ abstract createSdkClient( ...args: ConstructorParameters ): Promise; diff --git a/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts new file mode 100644 index 00000000000..16482e797b2 --- /dev/null +++ b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts @@ -0,0 +1,3 @@ +export abstract class SdkLoadService { + abstract load(): Promise; +} diff --git a/libs/common/src/platform/abstractions/sdk/sdk.service.ts b/libs/common/src/platform/abstractions/sdk/sdk.service.ts index 78ec11c4022..22ad2b44ff9 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk.service.ts @@ -3,6 +3,7 @@ import { Observable } from "rxjs"; import { BitwardenClient } from "@bitwarden/sdk-internal"; import { UserId } from "../../../types/guid"; +import { Rc } from "../../misc/reference-counting/rc"; export abstract class SdkService { /** @@ -27,5 +28,5 @@ export abstract class SdkService { * * @param userId */ - abstract userClient$(userId: UserId): Observable; + abstract userClient$(userId: UserId): Observable | undefined>; } diff --git a/libs/common/src/platform/misc/reference-counting/rc.spec.ts b/libs/common/src/platform/misc/reference-counting/rc.spec.ts new file mode 100644 index 00000000000..094abfe3615 --- /dev/null +++ b/libs/common/src/platform/misc/reference-counting/rc.spec.ts @@ -0,0 +1,93 @@ +// Temporary workaround for Symbol.dispose +// remove when https://github.com/jestjs/jest/issues/14874 is resolved and *released* +const disposeSymbol: unique symbol = Symbol("Symbol.dispose"); +const asyncDisposeSymbol: unique symbol = Symbol("Symbol.asyncDispose"); +(Symbol as any).asyncDispose ??= asyncDisposeSymbol as unknown as SymbolConstructor["asyncDispose"]; +(Symbol as any).dispose ??= disposeSymbol as unknown as SymbolConstructor["dispose"]; + +// Import needs to be after the workaround +import { Rc } from "./rc"; + +export class FreeableTestValue { + isFreed = false; + + free() { + this.isFreed = true; + } +} + +describe("Rc", () => { + let value: FreeableTestValue; + let rc: Rc; + + beforeEach(() => { + value = new FreeableTestValue(); + rc = new Rc(value); + }); + + it("should increase refCount when taken", () => { + rc.take(); + + expect(rc["refCount"]).toBe(1); + }); + + it("should return value on take", () => { + // eslint-disable-next-line @bitwarden/platform/required-using + const reference = rc.take(); + + expect(reference.value).toBe(value); + }); + + it("should decrease refCount when disposing reference", () => { + // eslint-disable-next-line @bitwarden/platform/required-using + const reference = rc.take(); + + reference[Symbol.dispose](); + + expect(rc["refCount"]).toBe(0); + }); + + it("should automatically decrease refCount when reference goes out of scope", () => { + { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + using reference = rc.take(); + } + + expect(rc["refCount"]).toBe(0); + }); + + it("should not free value when refCount reaches 0 if not marked for disposal", () => { + // eslint-disable-next-line @bitwarden/platform/required-using + const reference = rc.take(); + + reference[Symbol.dispose](); + + expect(value.isFreed).toBe(false); + }); + + it("should free value when refCount reaches 0 and rc is marked for disposal", () => { + // eslint-disable-next-line @bitwarden/platform/required-using + const reference = rc.take(); + rc.markForDisposal(); + + reference[Symbol.dispose](); + + expect(value.isFreed).toBe(true); + }); + + it("should free value when marked for disposal if refCount is 0", () => { + // eslint-disable-next-line @bitwarden/platform/required-using + const reference = rc.take(); + reference[Symbol.dispose](); + + rc.markForDisposal(); + + expect(value.isFreed).toBe(true); + }); + + it("should throw error when trying to take a disposed reference", () => { + rc.markForDisposal(); + + expect(() => rc.take()).toThrow(); + }); +}); diff --git a/libs/common/src/platform/misc/reference-counting/rc.ts b/libs/common/src/platform/misc/reference-counting/rc.ts new file mode 100644 index 00000000000..9be102b43d3 --- /dev/null +++ b/libs/common/src/platform/misc/reference-counting/rc.ts @@ -0,0 +1,76 @@ +import { UsingRequired } from "../using-required"; + +export type Freeable = { free: () => void }; + +/** + * Reference counted disposable value. + * This class is used to manage the lifetime of a value that needs to be + * freed of at a specific time but might still be in-use when that happens. + */ +export class Rc { + private markedForDisposal = false; + private refCount = 0; + private value: T; + + constructor(value: T) { + this.value = value; + } + + /** + * Use this function when you want to use the underlying object. + * This will guarantee that you have a reference to the object + * and that it won't be freed until your reference goes out of scope. + * + * This function must be used with the `using` keyword. + * + * @example + * ```typescript + * function someFunction(rc: Rc) { + * using reference = rc.take(); + * reference.value.doSomething(); + * // reference is automatically disposed here + * } + * ``` + * + * @returns The value. + */ + take(): Ref { + if (this.markedForDisposal) { + throw new Error("Cannot take a reference to a value marked for disposal"); + } + + this.refCount++; + return new Ref(() => this.release(), this.value); + } + + /** + * Mark this Rc for disposal. When the refCount reaches 0, the value + * will be freed. + */ + markForDisposal() { + this.markedForDisposal = true; + this.freeIfPossible(); + } + + private release() { + this.refCount--; + this.freeIfPossible(); + } + + private freeIfPossible() { + if (this.refCount === 0 && this.markedForDisposal) { + this.value.free(); + } + } +} + +export class Ref implements UsingRequired { + constructor( + private readonly release: () => void, + readonly value: T, + ) {} + + [Symbol.dispose]() { + this.release(); + } +} diff --git a/libs/common/src/platform/misc/support-status.ts b/libs/common/src/platform/misc/support-status.ts new file mode 100644 index 00000000000..6e02a10c8d8 --- /dev/null +++ b/libs/common/src/platform/misc/support-status.ts @@ -0,0 +1,48 @@ +import { ObservableInput, OperatorFunction, switchMap } from "rxjs"; + +/** + * Indicates that the given set of actions is not supported and there is + * not anything the user can do to make it supported. The reason property + * should contain a documented and machine readable string so more in + * depth details can be shown to the user. + */ +export type NotSupported = { type: "not-supported"; reason: string }; + +/** + * Indicates that the given set of actions does not currently work but + * could be supported if configuration, either inside Bitwarden or outside, + * is done. The reason property should contain a documented and + * machine readable string so further instruction can be supplied to the caller. + */ +export type NeedsConfiguration = { type: "needs-configuration"; reason: string }; + +/** + * Indicates that the actions in the service property are supported. + */ +export type Supported = { type: "supported"; service: T }; + +/** + * A type encapsulating the status of support for a service. + */ +export type SupportStatus = Supported | NeedsConfiguration | NotSupported; + +/** + * Projects each source value to one of the given projects defined in `selectors`. + * + * @param selectors.supported The function to run when the given item reports that it is supported + * @param selectors.notSupported The function to run when the given item reports that it is either not-supported + * or needs-configuration. + * @returns A function that returns an Observable that emits the result of one of the given projection functions. + */ +export function supportSwitch(selectors: { + supported: (service: TService, index: number) => ObservableInput; + notSupported: (reason: string, index: number) => ObservableInput; +}): OperatorFunction, TSupported | TNotSupported> { + return switchMap((supportStatus, index) => { + if (supportStatus.type === "supported") { + return selectors.supported(supportStatus.service, index); + } + + return selectors.notSupported(supportStatus.reason, index); + }); +} diff --git a/libs/common/src/platform/misc/using-required.ts b/libs/common/src/platform/misc/using-required.ts new file mode 100644 index 00000000000..f641b9f312f --- /dev/null +++ b/libs/common/src/platform/misc/using-required.ts @@ -0,0 +1,11 @@ +export type Disposable = { [Symbol.dispose]: () => void }; + +/** + * Types implementing this type must be used together with the `using` keyword + * + * @example using ref = rc.take(); + */ +// We want to use `interface` here because it creates a separate type. +// Type aliasing would not expose `UsingRequired` to the linter. +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UsingRequired extends Disposable {} diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index f654897e9e2..eaa8f0a813a 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -11,7 +11,7 @@ import { Merge } from "type-fest"; // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; -import { EncryptService } from "../abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../abstractions/i18n.service"; // FIXME: Remove when updating file. Eslint update diff --git a/libs/common/src/platform/models/data/server-config.data.spec.ts b/libs/common/src/platform/models/data/server-config.data.spec.ts index 13d14204085..d71e76657fd 100644 --- a/libs/common/src/platform/models/data/server-config.data.spec.ts +++ b/libs/common/src/platform/models/data/server-config.data.spec.ts @@ -1,3 +1,4 @@ +import { PushTechnology } from "../../../enums/push-technology.enum"; import { Region } from "../../abstractions/environment.service"; import { @@ -29,6 +30,9 @@ describe("ServerConfigData", () => { }, utcDate: "2020-01-01T00:00:00.000Z", featureStates: { feature: "state" }, + push: { + pushTechnology: PushTechnology.SignalR, + }, }; const serverConfigData = ServerConfigData.fromJSON(json); diff --git a/libs/common/src/platform/models/data/server-config.data.ts b/libs/common/src/platform/models/data/server-config.data.ts index 6ed51d2f5ce..af99f1d4a6d 100644 --- a/libs/common/src/platform/models/data/server-config.data.ts +++ b/libs/common/src/platform/models/data/server-config.data.ts @@ -9,6 +9,7 @@ import { ServerConfigResponse, ThirdPartyServerConfigResponse, EnvironmentServerConfigResponse, + PushSettingsConfigResponse, } from "../response/server-config.response"; export class ServerConfigData { @@ -18,6 +19,7 @@ export class ServerConfigData { environment?: EnvironmentServerConfigData; utcDate: string; featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; + push: PushSettingsConfigData; settings: ServerSettings; constructor(serverConfigResponse: Partial) { @@ -32,6 +34,9 @@ export class ServerConfigData { : null; this.featureStates = serverConfigResponse?.featureStates; this.settings = new ServerSettings(serverConfigResponse.settings); + this.push = serverConfigResponse?.push + ? new PushSettingsConfigData(serverConfigResponse.push) + : null; } static fromJSON(obj: Jsonify): ServerConfigData { @@ -42,6 +47,20 @@ export class ServerConfigData { } } +export class PushSettingsConfigData { + pushTechnology: number; + vapidPublicKey?: string; + + constructor(response: Partial) { + this.pushTechnology = response.pushTechnology; + this.vapidPublicKey = response.vapidPublicKey; + } + + static fromJSON(obj: Jsonify): PushSettingsConfigData { + return Object.assign(new PushSettingsConfigData({}), obj); + } +} + export class ThirdPartyServerConfigData { name: string; url: string; diff --git a/libs/common/src/platform/models/domain/domain-base.spec.ts b/libs/common/src/platform/models/domain/domain-base.spec.ts index 80a4e5e8606..0c13f9a2119 100644 --- a/libs/common/src/platform/models/domain/domain-base.spec.ts +++ b/libs/common/src/platform/models/domain/domain-base.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { makeEncString, makeSymmetricCryptoKey } from "../../../../spec"; -import { EncryptService } from "../../abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { Utils } from "../../misc/utils"; import Domain from "./domain-base"; diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index 110a1dc7208..192034254b9 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -2,8 +2,8 @@ // @ts-strict-ignore import { ConditionalExcept, ConditionalKeys, Constructor } from "type-fest"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { View } from "../../../models/view/view"; -import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString } from "./enc-string"; import { SymmetricCryptoKey } from "./symmetric-crypto-key"; diff --git a/libs/common/src/platform/models/domain/enc-string.spec.ts b/libs/common/src/platform/models/domain/enc-string.spec.ts index b4916b9f70a..9af19d36015 100644 --- a/libs/common/src/platform/models/domain/enc-string.spec.ts +++ b/libs/common/src/platform/models/domain/enc-string.spec.ts @@ -4,7 +4,7 @@ import { mock, MockProxy } from "jest-mock-extended"; // eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeEncString, makeStaticByteArray } from "../../../../spec"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { UserKey, OrgKey } from "../../../types/key"; import { EncryptionType } from "../../enums"; diff --git a/libs/common/src/platform/models/domain/enc-string.ts b/libs/common/src/platform/models/domain/enc-string.ts index f148664a4f9..a8fee428b13 100644 --- a/libs/common/src/platform/models/domain/enc-string.ts +++ b/libs/common/src/platform/models/domain/enc-string.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify, Opaque } from "type-fest"; -import { EncryptService } from "../../abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../enums"; import { Encrypted } from "../../interfaces/encrypted"; import { Utils } from "../../misc/utils"; diff --git a/libs/common/src/platform/models/response/server-config.response.ts b/libs/common/src/platform/models/response/server-config.response.ts index cae0603ea1e..afe98c2c349 100644 --- a/libs/common/src/platform/models/response/server-config.response.ts +++ b/libs/common/src/platform/models/response/server-config.response.ts @@ -11,6 +11,7 @@ export class ServerConfigResponse extends BaseResponse { server: ThirdPartyServerConfigResponse; environment: EnvironmentServerConfigResponse; featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; + push: PushSettingsConfigResponse; settings: ServerSettings; constructor(response: any) { @@ -25,10 +26,27 @@ export class ServerConfigResponse extends BaseResponse { this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server")); this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment")); this.featureStates = this.getResponseProperty("FeatureStates"); + this.push = new PushSettingsConfigResponse(this.getResponseProperty("Push")); this.settings = new ServerSettings(this.getResponseProperty("Settings")); } } +export class PushSettingsConfigResponse extends BaseResponse { + pushTechnology: number; + vapidPublicKey: string; + + constructor(data: any = null) { + super(data); + + if (data == null) { + return; + } + + this.pushTechnology = this.getResponseProperty("PushTechnology"); + this.vapidPublicKey = this.getResponseProperty("VapidPublicKey"); + } +} + export class EnvironmentServerConfigResponse extends BaseResponse { cloudRegion: Region; vault: string; diff --git a/libs/common/src/platform/notifications/index.ts b/libs/common/src/platform/notifications/index.ts new file mode 100644 index 00000000000..b1b842f5152 --- /dev/null +++ b/libs/common/src/platform/notifications/index.ts @@ -0,0 +1 @@ +export { NotificationsService } from "./notifications.service"; diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts new file mode 100644 index 00000000000..e24069a9fbe --- /dev/null +++ b/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts @@ -0,0 +1,316 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, Subject } from "rxjs"; + +import { LogoutReason } from "@bitwarden/auth/common"; + +import { awaitAsync } from "../../../../spec"; +import { Matrix } from "../../../../spec/matrix"; +import { AccountService } from "../../../auth/abstractions/account.service"; +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; +import { NotificationType } from "../../../enums"; +import { NotificationResponse } from "../../../models/response/notification.response"; +import { UserId } from "../../../types/guid"; +import { AppIdService } from "../../abstractions/app-id.service"; +import { Environment, EnvironmentService } from "../../abstractions/environment.service"; +import { LogService } from "../../abstractions/log.service"; +import { MessageSender } from "../../messaging"; +import { SupportStatus } from "../../misc/support-status"; +import { SyncService } from "../../sync"; + +import { + DefaultNotificationsService, + DISABLED_NOTIFICATIONS_URL, +} from "./default-notifications.service"; +import { SignalRConnectionService, SignalRNotification } from "./signalr-connection.service"; +import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service"; +import { WorkerWebPushConnectionService } from "./worker-webpush-connection.service"; + +describe("NotificationsService", () => { + let syncService: MockProxy; + let appIdService: MockProxy; + let environmentService: MockProxy; + let logoutCallback: jest.Mock, [logoutReason: LogoutReason]>; + let messagingService: MockProxy; + let accountService: MockProxy; + let signalRNotificationConnectionService: MockProxy; + let authService: MockProxy; + let webPushNotificationConnectionService: MockProxy; + + let activeAccount: BehaviorSubject>; + + let environment: BehaviorSubject>; + + let authStatusGetter: (userId: UserId) => BehaviorSubject; + + let webPushSupportGetter: (userId: UserId) => BehaviorSubject>; + + let signalrNotificationGetter: ( + userId: UserId, + notificationsUrl: string, + ) => Subject; + + let sut: DefaultNotificationsService; + + beforeEach(() => { + syncService = mock(); + appIdService = mock(); + environmentService = mock(); + logoutCallback = jest.fn, [logoutReason: LogoutReason]>(); + messagingService = mock(); + accountService = mock(); + signalRNotificationConnectionService = mock(); + authService = mock(); + webPushNotificationConnectionService = mock(); + + activeAccount = new BehaviorSubject>(null); + accountService.activeAccount$ = activeAccount.asObservable(); + + environment = new BehaviorSubject>({ + getNotificationsUrl: () => "https://notifications.bitwarden.com", + } as Environment); + + environmentService.environment$ = environment; + + authStatusGetter = Matrix.autoMockMethod( + authService.authStatusFor$, + () => new BehaviorSubject(AuthenticationStatus.LoggedOut), + ); + + webPushSupportGetter = Matrix.autoMockMethod( + webPushNotificationConnectionService.supportStatus$, + () => + new BehaviorSubject>({ + type: "not-supported", + reason: "test", + }), + ); + + signalrNotificationGetter = Matrix.autoMockMethod( + signalRNotificationConnectionService.connect$, + () => new Subject(), + ); + + sut = new DefaultNotificationsService( + mock(), + syncService, + appIdService, + environmentService, + logoutCallback, + messagingService, + accountService, + signalRNotificationConnectionService, + authService, + webPushNotificationConnectionService, + ); + }); + + const mockUser1 = "user1" as UserId; + const mockUser2 = "user2" as UserId; + + function emitActiveUser(userId: UserId) { + if (userId == null) { + activeAccount.next(null); + } else { + activeAccount.next({ id: userId, email: "email", name: "Test Name", emailVerified: true }); + } + } + + function emitNotificationUrl(url: string) { + environment.next({ + getNotificationsUrl: () => url, + } as Environment); + } + + const expectNotification = ( + notification: readonly [NotificationResponse, UserId], + expectedUser: UserId, + expectedType: NotificationType, + ) => { + const [actualNotification, actualUser] = notification; + expect(actualUser).toBe(expectedUser); + expect(actualNotification.type).toBe(expectedType); + }; + + it("emits notifications through WebPush when supported", async () => { + const notificationsPromise = firstValueFrom(sut.notifications$.pipe(bufferCount(2))); + + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.Unlocked); + + const webPush = mock(); + const webPushSubject = new Subject(); + webPush.notifications$ = webPushSubject; + + webPushSupportGetter(mockUser1).next({ type: "supported", service: webPush }); + webPushSubject.next(new NotificationResponse({ type: NotificationType.SyncFolderCreate })); + webPushSubject.next(new NotificationResponse({ type: NotificationType.SyncFolderDelete })); + + const notifications = await notificationsPromise; + expectNotification(notifications[0], mockUser1, NotificationType.SyncFolderCreate); + expectNotification(notifications[1], mockUser1, NotificationType.SyncFolderDelete); + }); + + it("switches to SignalR when web push is not supported.", async () => { + const notificationsPromise = firstValueFrom(sut.notifications$.pipe(bufferCount(2))); + + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.Unlocked); + + const webPush = mock(); + const webPushSubject = new Subject(); + webPush.notifications$ = webPushSubject; + + webPushSupportGetter(mockUser1).next({ type: "supported", service: webPush }); + webPushSubject.next(new NotificationResponse({ type: NotificationType.SyncFolderCreate })); + + emitActiveUser(mockUser2); + authStatusGetter(mockUser2).next(AuthenticationStatus.Unlocked); + // Second user does not support web push + webPushSupportGetter(mockUser2).next({ type: "not-supported", reason: "test" }); + + signalrNotificationGetter(mockUser2, "http://test.example.com").next({ + type: "ReceiveMessage", + message: new NotificationResponse({ type: NotificationType.SyncCipherUpdate }), + }); + + const notifications = await notificationsPromise; + expectNotification(notifications[0], mockUser1, NotificationType.SyncFolderCreate); + expectNotification(notifications[1], mockUser2, NotificationType.SyncCipherUpdate); + }); + + it("switches to WebPush when it becomes supported.", async () => { + const notificationsPromise = firstValueFrom(sut.notifications$.pipe(bufferCount(2))); + + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.Unlocked); + webPushSupportGetter(mockUser1).next({ type: "not-supported", reason: "test" }); + + signalrNotificationGetter(mockUser1, "http://test.example.com").next({ + type: "ReceiveMessage", + message: new NotificationResponse({ type: NotificationType.AuthRequest }), + }); + + const webPush = mock(); + const webPushSubject = new Subject(); + webPush.notifications$ = webPushSubject; + + webPushSupportGetter(mockUser1).next({ type: "supported", service: webPush }); + webPushSubject.next(new NotificationResponse({ type: NotificationType.SyncLoginDelete })); + + const notifications = await notificationsPromise; + expectNotification(notifications[0], mockUser1, NotificationType.AuthRequest); + expectNotification(notifications[1], mockUser1, NotificationType.SyncLoginDelete); + }); + + it("does not emit SignalR heartbeats", async () => { + const notificationsPromise = firstValueFrom(sut.notifications$.pipe(bufferCount(1))); + + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.Unlocked); + webPushSupportGetter(mockUser1).next({ type: "not-supported", reason: "test" }); + + signalrNotificationGetter(mockUser1, "http://test.example.com").next({ type: "Heartbeat" }); + signalrNotificationGetter(mockUser1, "http://test.example.com").next({ + type: "ReceiveMessage", + message: new NotificationResponse({ type: NotificationType.AuthRequestResponse }), + }); + + const notifications = await notificationsPromise; + expectNotification(notifications[0], mockUser1, NotificationType.AuthRequestResponse); + }); + + it.each([ + { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked }, + { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked }, + { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked }, + { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Unlocked }, + ])( + "does not re-connect when the user transitions from $initialStatus to $updatedStatus", + async ({ initialStatus, updatedStatus }) => { + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(initialStatus); + webPushSupportGetter(mockUser1).next({ type: "not-supported", reason: "test" }); + + const notificationsSubscriptions = sut.notifications$.subscribe(); + await awaitAsync(1); + + authStatusGetter(mockUser1).next(updatedStatus); + await awaitAsync(1); + + expect(signalRNotificationConnectionService.connect$).toHaveBeenCalledTimes(1); + expect(signalRNotificationConnectionService.connect$).toHaveBeenCalledWith( + mockUser1, + "http://test.example.com", + ); + notificationsSubscriptions.unsubscribe(); + }, + ); + + it.each([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked])( + "connects when a user transitions from logged out to %s", + async (newStatus: AuthenticationStatus) => { + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.LoggedOut); + webPushSupportGetter(mockUser1).next({ type: "not-supported", reason: "test" }); + + const notificationsSubscriptions = sut.notifications$.subscribe(); + await awaitAsync(1); + + authStatusGetter(mockUser1).next(newStatus); + await awaitAsync(1); + + expect(signalRNotificationConnectionService.connect$).toHaveBeenCalledTimes(1); + expect(signalRNotificationConnectionService.connect$).toHaveBeenCalledWith( + mockUser1, + "http://test.example.com", + ); + notificationsSubscriptions.unsubscribe(); + }, + ); + + it("does not connect to any notification stream when notifications are disabled through special url", () => { + const subscription = sut.notifications$.subscribe(); + emitActiveUser(mockUser1); + emitNotificationUrl(DISABLED_NOTIFICATIONS_URL); + + expect(signalRNotificationConnectionService.connect$).not.toHaveBeenCalled(); + expect(webPushNotificationConnectionService.supportStatus$).not.toHaveBeenCalled(); + + subscription.unsubscribe(); + }); + + it("does not connect to any notification stream when there is no active user", () => { + const subscription = sut.notifications$.subscribe(); + emitActiveUser(null); + + expect(signalRNotificationConnectionService.connect$).not.toHaveBeenCalled(); + expect(webPushNotificationConnectionService.supportStatus$).not.toHaveBeenCalled(); + + subscription.unsubscribe(); + }); + + it("does not reconnect if the same notification url is emitted", async () => { + const subscription = sut.notifications$.subscribe(); + + emitActiveUser(mockUser1); + emitNotificationUrl("http://test.example.com"); + authStatusGetter(mockUser1).next(AuthenticationStatus.Unlocked); + + await awaitAsync(1); + + expect(webPushNotificationConnectionService.supportStatus$).toHaveBeenCalledTimes(1); + emitNotificationUrl("http://test.example.com"); + + await awaitAsync(1); + + expect(webPushNotificationConnectionService.supportStatus$).toHaveBeenCalledTimes(1); + subscription.unsubscribe(); + }); +}); diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.ts new file mode 100644 index 00000000000..c6b330857a4 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/default-notifications.service.ts @@ -0,0 +1,238 @@ +import { + BehaviorSubject, + catchError, + distinctUntilChanged, + EMPTY, + filter, + map, + mergeMap, + Observable, + switchMap, +} from "rxjs"; + +import { LogoutReason } from "@bitwarden/auth/common"; + +import { AccountService } from "../../../auth/abstractions/account.service"; +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; +import { NotificationType } from "../../../enums"; +import { + NotificationResponse, + SyncCipherNotification, + SyncFolderNotification, + SyncSendNotification, +} from "../../../models/response/notification.response"; +import { UserId } from "../../../types/guid"; +import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; +import { AppIdService } from "../../abstractions/app-id.service"; +import { EnvironmentService } from "../../abstractions/environment.service"; +import { LogService } from "../../abstractions/log.service"; +import { MessagingService } from "../../abstractions/messaging.service"; +import { supportSwitch } from "../../misc/support-status"; +import { NotificationsService as NotificationsServiceAbstraction } from "../notifications.service"; + +import { ReceiveMessage, SignalRConnectionService } from "./signalr-connection.service"; +import { WebPushConnectionService } from "./webpush-connection.service"; + +export const DISABLED_NOTIFICATIONS_URL = "http://-"; + +export class DefaultNotificationsService implements NotificationsServiceAbstraction { + notifications$: Observable; + + private activitySubject = new BehaviorSubject<"active" | "inactive">("active"); + + constructor( + private readonly logService: LogService, + private syncService: SyncService, + private appIdService: AppIdService, + private environmentService: EnvironmentService, + private logoutCallback: (logoutReason: LogoutReason, userId: UserId) => Promise, + private messagingService: MessagingService, + private readonly accountService: AccountService, + private readonly signalRConnectionService: SignalRConnectionService, + private readonly authService: AuthService, + private readonly webPushConnectionService: WebPushConnectionService, + ) { + this.notifications$ = this.accountService.activeAccount$.pipe( + map((account) => account?.id), + distinctUntilChanged(), + switchMap((activeAccountId) => { + if (activeAccountId == null) { + // We don't emit notifications for inactive accounts currently + return EMPTY; + } + + return this.userNotifications$(activeAccountId).pipe( + map((notification) => [notification, activeAccountId] as const), + ); + }), + ); + } + + /** + * Retrieves a stream of push notifications for the given user. + * @param userId The user id of the user to get the push notifications for. + */ + private userNotifications$(userId: UserId) { + return this.environmentService.environment$.pipe( + map((env) => env.getNotificationsUrl()), + distinctUntilChanged(), + switchMap((notificationsUrl) => { + if (notificationsUrl === DISABLED_NOTIFICATIONS_URL) { + return EMPTY; + } + + return this.userNotificationsHelper$(userId, notificationsUrl); + }), + ); + } + + private userNotificationsHelper$(userId: UserId, notificationsUrl: string) { + return this.hasAccessToken$(userId).pipe( + switchMap((hasAccessToken) => { + if (!hasAccessToken) { + return EMPTY; + } + + return this.activitySubject; + }), + switchMap((activityStatus) => { + if (activityStatus === "inactive") { + return EMPTY; + } + + return this.webPushConnectionService.supportStatus$(userId); + }), + supportSwitch({ + supported: (service) => + service.notifications$.pipe( + catchError((err: unknown) => { + this.logService.warning("Issue with web push, falling back to SignalR", err); + return this.connectSignalR$(userId, notificationsUrl); + }), + ), + notSupported: () => this.connectSignalR$(userId, notificationsUrl), + }), + ); + } + + private connectSignalR$(userId: UserId, notificationsUrl: string) { + return this.signalRConnectionService.connect$(userId, notificationsUrl).pipe( + filter((n) => n.type === "ReceiveMessage"), + map((n) => (n as ReceiveMessage).message), + ); + } + + private hasAccessToken$(userId: UserId) { + return this.authService.authStatusFor$(userId).pipe( + map( + (authStatus) => + authStatus === AuthenticationStatus.Locked || + authStatus === AuthenticationStatus.Unlocked, + ), + distinctUntilChanged(), + ); + } + + private async processNotification(notification: NotificationResponse, userId: UserId) { + const appId = await this.appIdService.getAppId(); + if (notification == null || notification.contextId === appId) { + return; + } + + const payloadUserId = notification.payload?.userId || notification.payload?.UserId; + if (payloadUserId != null && payloadUserId !== userId) { + return; + } + + switch (notification.type) { + case NotificationType.SyncCipherCreate: + case NotificationType.SyncCipherUpdate: + await this.syncService.syncUpsertCipher( + notification.payload as SyncCipherNotification, + notification.type === NotificationType.SyncCipherUpdate, + ); + break; + case NotificationType.SyncCipherDelete: + case NotificationType.SyncLoginDelete: + await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); + break; + case NotificationType.SyncFolderCreate: + case NotificationType.SyncFolderUpdate: + await this.syncService.syncUpsertFolder( + notification.payload as SyncFolderNotification, + notification.type === NotificationType.SyncFolderUpdate, + userId, + ); + break; + case NotificationType.SyncFolderDelete: + await this.syncService.syncDeleteFolder( + notification.payload as SyncFolderNotification, + userId, + ); + break; + case NotificationType.SyncVault: + case NotificationType.SyncCiphers: + case NotificationType.SyncSettings: + await this.syncService.fullSync(false); + + break; + case NotificationType.SyncOrganizations: + // An organization update may not have bumped the user's account revision date, so force a sync + await this.syncService.fullSync(true); + break; + case NotificationType.SyncOrgKeys: + await this.syncService.fullSync(true); + this.activitySubject.next("inactive"); // Force a disconnect + this.activitySubject.next("active"); // Allow a reconnect + break; + case NotificationType.LogOut: + this.logService.info("[Notifications Service] Received logout notification"); + await this.logoutCallback("logoutNotification", userId); + break; + case NotificationType.SyncSendCreate: + case NotificationType.SyncSendUpdate: + await this.syncService.syncUpsertSend( + notification.payload as SyncSendNotification, + notification.type === NotificationType.SyncSendUpdate, + ); + break; + case NotificationType.SyncSendDelete: + await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification); + break; + case NotificationType.AuthRequest: + { + this.messagingService.send("openLoginApproval", { + notificationId: notification.payload.id, + }); + } + break; + case NotificationType.SyncOrganizationStatusChanged: + await this.syncService.fullSync(true); + break; + case NotificationType.SyncOrganizationCollectionSettingChanged: + await this.syncService.fullSync(true); + break; + default: + break; + } + } + + startListening() { + return this.notifications$ + .pipe( + mergeMap(async ([notification, userId]) => this.processNotification(notification, userId)), + ) + .subscribe({ + error: (e: unknown) => this.logService.warning("Error in notifications$ observable", e), + }); + } + + reconnectFromActivity(): void { + this.activitySubject.next("active"); + } + + disconnectFromInactivity(): void { + this.activitySubject.next("inactive"); + } +} diff --git a/libs/common/src/platform/notifications/internal/index.ts b/libs/common/src/platform/notifications/internal/index.ts new file mode 100644 index 00000000000..067320ee56c --- /dev/null +++ b/libs/common/src/platform/notifications/internal/index.ts @@ -0,0 +1,8 @@ +export * from "./worker-webpush-connection.service"; +export * from "./signalr-connection.service"; +export * from "./default-notifications.service"; +export * from "./noop-notifications.service"; +export * from "./unsupported-webpush-connection.service"; +export * from "./webpush-connection.service"; +export * from "./websocket-webpush-connection.service"; +export * from "./web-push-notifications-api.service"; diff --git a/libs/common/src/platform/notifications/internal/noop-notifications.service.ts b/libs/common/src/platform/notifications/internal/noop-notifications.service.ts new file mode 100644 index 00000000000..f79cabfca8a --- /dev/null +++ b/libs/common/src/platform/notifications/internal/noop-notifications.service.ts @@ -0,0 +1,23 @@ +import { Subscription } from "rxjs"; + +import { LogService } from "../../abstractions/log.service"; +import { NotificationsService } from "../notifications.service"; + +export class NoopNotificationsService implements NotificationsService { + constructor(private logService: LogService) {} + + startListening(): Subscription { + this.logService.info( + "Initializing no-op notification service, no push notifications will be received", + ); + return Subscription.EMPTY; + } + + reconnectFromActivity(): void { + this.logService.info("Reconnecting notification service from activity"); + } + + disconnectFromInactivity(): void { + this.logService.info("Disconnecting notification service from inactivity"); + } +} diff --git a/libs/common/src/platform/notifications/internal/signalr-connection.service.ts b/libs/common/src/platform/notifications/internal/signalr-connection.service.ts new file mode 100644 index 00000000000..e5d210266c0 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/signalr-connection.service.ts @@ -0,0 +1,125 @@ +import { + HttpTransportType, + HubConnectionBuilder, + HubConnectionState, + ILogger, + LogLevel, +} from "@microsoft/signalr"; +import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; +import { Observable, Subscription } from "rxjs"; + +import { ApiService } from "../../../abstractions/api.service"; +import { NotificationResponse } from "../../../models/response/notification.response"; +import { UserId } from "../../../types/guid"; +import { LogService } from "../../abstractions/log.service"; + +// 2 Minutes +const MIN_RECONNECT_TIME = 2 * 60 * 1000; +// 5 Minutes +const MAX_RECONNECT_TIME = 5 * 60 * 1000; + +export type Heartbeat = { type: "Heartbeat" }; +export type ReceiveMessage = { type: "ReceiveMessage"; message: NotificationResponse }; + +export type SignalRNotification = Heartbeat | ReceiveMessage; + +class SignalRLogger implements ILogger { + constructor(private readonly logService: LogService) {} + + log(logLevel: LogLevel, message: string): void { + switch (logLevel) { + case LogLevel.Critical: + this.logService.error(message); + break; + case LogLevel.Error: + this.logService.error(message); + break; + case LogLevel.Warning: + this.logService.warning(message); + break; + case LogLevel.Information: + this.logService.info(message); + break; + case LogLevel.Debug: + this.logService.debug(message); + break; + } + } +} + +export class SignalRConnectionService { + constructor( + private readonly apiService: ApiService, + private readonly logService: LogService, + ) {} + + connect$(userId: UserId, notificationsUrl: string) { + return new Observable((subsciber) => { + const connection = new HubConnectionBuilder() + .withUrl(notificationsUrl + "/hub", { + accessTokenFactory: () => this.apiService.getActiveBearerToken(), + skipNegotiation: true, + transport: HttpTransportType.WebSockets, + }) + .withHubProtocol(new MessagePackHubProtocol()) + .configureLogging(new SignalRLogger(this.logService)) + .build(); + + connection.on("ReceiveMessage", (data: any) => { + subsciber.next({ type: "ReceiveMessage", message: new NotificationResponse(data) }); + }); + + connection.on("Heartbeat", () => { + subsciber.next({ type: "Heartbeat" }); + }); + + let reconnectSubscription: Subscription | null = null; + + // Create schedule reconnect function + const scheduleReconnect = (): Subscription => { + if ( + connection == null || + connection.state !== HubConnectionState.Disconnected || + (reconnectSubscription != null && !reconnectSubscription.closed) + ) { + return Subscription.EMPTY; + } + + const randomTime = this.random(); + const timeoutHandler = setTimeout(() => { + connection + .start() + .then(() => (reconnectSubscription = null)) + .catch(() => { + reconnectSubscription = scheduleReconnect(); + }); + }, randomTime); + + return new Subscription(() => clearTimeout(timeoutHandler)); + }; + + connection.onclose((error) => { + reconnectSubscription = scheduleReconnect(); + }); + + // Start connection + connection.start().catch(() => { + reconnectSubscription = scheduleReconnect(); + }); + + return () => { + connection?.stop().catch((error) => { + this.logService.error("Error while stopping SignalR connection", error); + // TODO: Does calling stop call `onclose`? + reconnectSubscription?.unsubscribe(); + }); + }; + }); + } + + private random() { + return ( + Math.floor(Math.random() * (MAX_RECONNECT_TIME - MIN_RECONNECT_TIME + 1)) + MIN_RECONNECT_TIME + ); + } +} diff --git a/libs/common/src/platform/notifications/internal/unsupported-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/unsupported-webpush-connection.service.ts new file mode 100644 index 00000000000..0016a882949 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/unsupported-webpush-connection.service.ts @@ -0,0 +1,15 @@ +import { Observable, of } from "rxjs"; + +import { UserId } from "../../../types/guid"; +import { SupportStatus } from "../../misc/support-status"; + +import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service"; + +/** + * An implementation of {@see WebPushConnectionService} for clients that do not have support for WebPush + */ +export class UnsupportedWebPushConnectionService implements WebPushConnectionService { + supportStatus$(userId: UserId): Observable> { + return of({ type: "not-supported", reason: "client-not-supported" }); + } +} diff --git a/libs/common/src/platform/notifications/internal/web-push-notifications-api.service.ts b/libs/common/src/platform/notifications/internal/web-push-notifications-api.service.ts new file mode 100644 index 00000000000..b824b8c7d65 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/web-push-notifications-api.service.ts @@ -0,0 +1,25 @@ +import { ApiService } from "../../../abstractions/api.service"; +import { AppIdService } from "../../abstractions/app-id.service"; + +import { WebPushRequest } from "./web-push.request"; + +export class WebPushNotificationsApiService { + constructor( + private readonly apiService: ApiService, + private readonly appIdService: AppIdService, + ) {} + + /** + * Posts a device-user association to the server and ensures it's installed for push notifications + */ + async putSubscription(pushSubscription: PushSubscriptionJSON): Promise { + const request = WebPushRequest.from(pushSubscription); + await this.apiService.send( + "POST", + `/devices/identifier/${await this.appIdService.getAppId()}/web-push-auth`, + request, + true, + false, + ); + } +} diff --git a/libs/common/src/platform/notifications/internal/web-push.request.ts b/libs/common/src/platform/notifications/internal/web-push.request.ts new file mode 100644 index 00000000000..c6375986324 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/web-push.request.ts @@ -0,0 +1,13 @@ +export class WebPushRequest { + endpoint: string | undefined; + p256dh: string | undefined; + auth: string | undefined; + + static from(pushSubscription: PushSubscriptionJSON): WebPushRequest { + const result = new WebPushRequest(); + result.endpoint = pushSubscription.endpoint; + result.p256dh = pushSubscription.keys?.p256dh; + result.auth = pushSubscription.keys?.auth; + return result; + } +} diff --git a/libs/common/src/platform/notifications/internal/webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/webpush-connection.service.ts new file mode 100644 index 00000000000..17ef87ea83e --- /dev/null +++ b/libs/common/src/platform/notifications/internal/webpush-connection.service.ts @@ -0,0 +1,13 @@ +import { Observable } from "rxjs"; + +import { NotificationResponse } from "../../../models/response/notification.response"; +import { UserId } from "../../../types/guid"; +import { SupportStatus } from "../../misc/support-status"; + +export interface WebPushConnector { + notifications$: Observable; +} + +export abstract class WebPushConnectionService { + abstract supportStatus$(userId: UserId): Observable>; +} diff --git a/libs/common/src/platform/notifications/internal/websocket-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/websocket-webpush-connection.service.ts new file mode 100644 index 00000000000..7a25fb4ce50 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/websocket-webpush-connection.service.ts @@ -0,0 +1,12 @@ +import { Observable, of } from "rxjs"; + +import { UserId } from "../../../types/guid"; +import { SupportStatus } from "../../misc/support-status"; + +import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service"; + +export class WebSocketWebPushConnectionService implements WebPushConnectionService { + supportStatus$(userId: UserId): Observable> { + return of({ type: "not-supported", reason: "work-in-progress" }); + } +} diff --git a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts new file mode 100644 index 00000000000..631c624d667 --- /dev/null +++ b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts @@ -0,0 +1,168 @@ +import { + concat, + concatMap, + defer, + distinctUntilChanged, + fromEvent, + map, + Observable, + Subject, + Subscription, + switchMap, +} from "rxjs"; + +import { PushTechnology } from "../../../enums/push-technology.enum"; +import { NotificationResponse } from "../../../models/response/notification.response"; +import { UserId } from "../../../types/guid"; +import { ConfigService } from "../../abstractions/config/config.service"; +import { SupportStatus } from "../../misc/support-status"; +import { Utils } from "../../misc/utils"; + +import { WebPushNotificationsApiService } from "./web-push-notifications-api.service"; +import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service"; + +// Ref: https://w3c.github.io/push-api/#the-pushsubscriptionchange-event +interface PushSubscriptionChangeEvent { + readonly newSubscription?: PushSubscription; + readonly oldSubscription?: PushSubscription; +} + +// Ref: https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData +interface PushMessageData { + json(): any; +} + +// Ref: https://developer.mozilla.org/en-US/docs/Web/API/PushEvent +interface PushEvent { + data: PushMessageData; +} + +/** + * An implementation for connecting to web push based notifications running in a Worker. + */ +export class WorkerWebPushConnectionService implements WebPushConnectionService { + private pushEvent = new Subject(); + private pushChangeEvent = new Subject(); + + constructor( + private readonly configService: ConfigService, + private readonly webPushApiService: WebPushNotificationsApiService, + private readonly serviceWorkerRegistration: ServiceWorkerRegistration, + ) {} + + start(): Subscription { + const subscription = new Subscription(() => { + this.pushEvent.complete(); + this.pushChangeEvent.complete(); + this.pushEvent = new Subject(); + this.pushChangeEvent = new Subject(); + }); + + const pushEventSubscription = fromEvent(self, "push").subscribe(this.pushEvent); + + const pushChangeEventSubscription = fromEvent( + self, + "pushsubscriptionchange", + ).subscribe(this.pushChangeEvent); + + subscription.add(pushEventSubscription); + subscription.add(pushChangeEventSubscription); + + return subscription; + } + + supportStatus$(userId: UserId): Observable> { + // Check the server config to see if it supports sending WebPush notifications + // FIXME: get config of server for the specified userId, once ConfigService supports it + return this.configService.serverConfig$.pipe( + map((config) => + config?.push?.pushTechnology === PushTechnology.WebPush ? config.push.vapidPublicKey : null, + ), + // No need to re-emit when there is new server config if the vapidPublicKey is still there and the exact same + distinctUntilChanged(), + map((publicKey) => { + if (publicKey == null) { + return { + type: "not-supported", + reason: "server-not-configured", + } satisfies SupportStatus; + } + + return { + type: "supported", + service: new MyWebPushConnector( + publicKey, + userId, + this.webPushApiService, + this.serviceWorkerRegistration, + this.pushEvent, + this.pushChangeEvent, + ), + } satisfies SupportStatus; + }), + ); + } +} + +class MyWebPushConnector implements WebPushConnector { + notifications$: Observable; + + constructor( + private readonly vapidPublicKey: string, + private readonly userId: UserId, + private readonly webPushApiService: WebPushNotificationsApiService, + private readonly serviceWorkerRegistration: ServiceWorkerRegistration, + private readonly pushEvent$: Observable, + private readonly pushChangeEvent$: Observable, + ) { + this.notifications$ = this.getOrCreateSubscription$(this.vapidPublicKey).pipe( + concatMap((subscription) => { + return defer(() => { + if (subscription == null) { + throw new Error("Expected a non-null subscription."); + } + return this.webPushApiService.putSubscription(subscription.toJSON()); + }).pipe( + switchMap(() => this.pushEvent$), + map((e) => new NotificationResponse(e.data.json().data)), + ); + }), + ); + } + + private async pushManagerSubscribe(key: string) { + return await this.serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: key, + }); + } + + private getOrCreateSubscription$(key: string) { + return concat( + defer(async () => { + const existingSubscription = + await this.serviceWorkerRegistration.pushManager.getSubscription(); + + if (existingSubscription == null) { + return await this.pushManagerSubscribe(key); + } + + const subscriptionKey = Utils.fromBufferToUrlB64( + // REASON: `Utils.fromBufferToUrlB64` handles null by returning null back to it. + // its annotation should be updated and then this assertion can be removed. + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + existingSubscription.options?.applicationServerKey!, + ); + + if (subscriptionKey !== key) { + // There is a subscription, but it's not for the current server, unsubscribe and then make a new one + await existingSubscription.unsubscribe(); + return await this.pushManagerSubscribe(key); + } + + return existingSubscription; + }), + this.pushChangeEvent$.pipe(map((event) => event.newSubscription)), + ); + } +} diff --git a/libs/common/src/platform/notifications/notifications.service.ts b/libs/common/src/platform/notifications/notifications.service.ts new file mode 100644 index 00000000000..aa4ff2a57a6 --- /dev/null +++ b/libs/common/src/platform/notifications/notifications.service.ts @@ -0,0 +1,18 @@ +import { Subscription } from "rxjs"; + +/** + * A service offering abilities to interact with push notifications from the server. + */ +export abstract class NotificationsService { + /** + * Starts automatic listening and processing of notifications, should only be called once per application, + * or you will risk notifications being processed multiple times. + */ + abstract startListening(): Subscription; + // TODO: Delete this method in favor of an `ActivityService` that notifications can depend on. + // https://bitwarden.atlassian.net/browse/PM-14264 + abstract reconnectFromActivity(): void; + // TODO: Delete this method in favor of an `ActivityService` that notifications can depend on. + // https://bitwarden.atlassian.net/browse/PM-14264 + abstract disconnectFromInactivity(): void; +} diff --git a/libs/common/src/platform/services/container.service.ts b/libs/common/src/platform/services/container.service.ts index c3e727a2e1e..7421de8cc2c 100644 --- a/libs/common/src/platform/services/container.service.ts +++ b/libs/common/src/platform/services/container.service.ts @@ -1,7 +1,7 @@ // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; -import { EncryptService } from "../abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; export class ContainerService { constructor( diff --git a/libs/common/src/platform/services/noop-notifications.service.ts b/libs/common/src/platform/services/noop-notifications.service.ts deleted file mode 100644 index edfeccd322d..00000000000 --- a/libs/common/src/platform/services/noop-notifications.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NotificationsService as NotificationsServiceAbstraction } from "../../abstractions/notifications.service"; -import { LogService } from "../abstractions/log.service"; - -export class NoopNotificationsService implements NotificationsServiceAbstraction { - constructor(private logService: LogService) {} - - init(): Promise { - this.logService.info( - "Initializing no-op notification service, no push notifications will be received", - ); - return Promise.resolve(); - } - - updateConnection(sync?: boolean): Promise { - this.logService.info("Updating notification service connection"); - return Promise.resolve(); - } - - reconnectFromActivity(): Promise { - this.logService.info("Reconnecting notification service from activity"); - return Promise.resolve(); - } - - disconnectFromInactivity(): Promise { - this.logService.info("Disconnecting notification service from inactivity"); - return Promise.resolve(); - } -} diff --git a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts index 8e99af2efed..fc55cc83ac8 100644 --- a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts +++ b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts @@ -1,19 +1,19 @@ import * as sdk from "@bitwarden/sdk-internal"; -import * as module from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; /** - * Directly imports the Bitwarden SDK and initializes it. - * - * **Warning**: This requires WASM support and will fail if the environment does not support it. + * Default SDK client factory. */ export class DefaultSdkClientFactory implements SdkClientFactory { + /** + * Initializes a Bitwarden client. Assumes the SDK is already loaded. + * @param args Bitwarden client constructor parameters + * @returns A BitwardenClient + */ async createSdkClient( ...args: ConstructorParameters ): Promise { - (sdk as any).init(module); - return Promise.resolve(new sdk.BitwardenClient(...args)); } } diff --git a/libs/common/src/platform/services/sdk/default-sdk-load.service.ts b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts new file mode 100644 index 00000000000..eff641f0351 --- /dev/null +++ b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts @@ -0,0 +1,15 @@ +import * as sdk from "@bitwarden/sdk-internal"; +import * as bitwardenModule from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; + +import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; + +/** + * Directly imports the Bitwarden SDK and initializes it. + * + * **Warning**: This requires WASM support and will fail if the environment does not support it. + */ +export class DefaultSdkLoadService implements SdkLoadService { + async load(): Promise { + (sdk as any).init(bitwardenModule); + } +} diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts index c5917e0230f..e8dfde863ec 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -10,6 +10,7 @@ import { UserKey } from "../../../types/key"; import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; +import { Rc } from "../../misc/reference-counting/rc"; import { EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; @@ -75,15 +76,14 @@ describe("DefaultSdkService", () => { }); it("creates an SDK client when called the first time", async () => { - const result = await firstValueFrom(service.userClient$(userId)); + await firstValueFrom(service.userClient$(userId)); - expect(result).toBe(mockClient); expect(sdkClientFactory.createSdkClient).toHaveBeenCalled(); }); it("does not create an SDK client when called the second time with same userId", async () => { - const subject_1 = new BehaviorSubject(undefined); - const subject_2 = new BehaviorSubject(undefined); + const subject_1 = new BehaviorSubject | undefined>(undefined); + const subject_2 = new BehaviorSubject | undefined>(undefined); // Use subjects to ensure the subscription is kept alive service.userClient$(userId).subscribe(subject_1); @@ -92,14 +92,14 @@ describe("DefaultSdkService", () => { // Wait for the next tick to ensure all async operations are done await new Promise(process.nextTick); - expect(subject_1.value).toBe(mockClient); - expect(subject_2.value).toBe(mockClient); + expect(subject_1.value.take().value).toBe(mockClient); + expect(subject_2.value.take().value).toBe(mockClient); expect(sdkClientFactory.createSdkClient).toHaveBeenCalledTimes(1); }); it("destroys the SDK client when all subscriptions are closed", async () => { - const subject_1 = new BehaviorSubject(undefined); - const subject_2 = new BehaviorSubject(undefined); + const subject_1 = new BehaviorSubject | undefined>(undefined); + const subject_2 = new BehaviorSubject | undefined>(undefined); const subscription_1 = service.userClient$(userId).subscribe(subject_1); const subscription_2 = service.userClient$(userId).subscribe(subject_2); await new Promise(process.nextTick); @@ -107,6 +107,7 @@ describe("DefaultSdkService", () => { subscription_1.unsubscribe(); subscription_2.unsubscribe(); + await new Promise(process.nextTick); expect(mockClient.free).toHaveBeenCalledTimes(1); }); @@ -114,7 +115,7 @@ describe("DefaultSdkService", () => { const userKey$ = new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey); keyService.userKey$.calledWith(userId).mockReturnValue(userKey$); - const subject = new BehaviorSubject(undefined); + const subject = new BehaviorSubject | undefined>(undefined); service.userClient$(userId).subscribe(subject); await new Promise(process.nextTick); diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index e9cecbb15dc..516334c7fb4 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -30,10 +30,11 @@ import { PlatformUtilsService } from "../../abstractions/platform-utils.service" import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; import { SdkService } from "../../abstractions/sdk/sdk.service"; import { compareValues } from "../../misc/compare-values"; +import { Rc } from "../../misc/reference-counting/rc"; import { EncryptedString } from "../../models/domain/enc-string"; export class DefaultSdkService implements SdkService { - private sdkClientCache = new Map>(); + private sdkClientCache = new Map>>(); client$ = this.environmentService.environment$.pipe( concatMap(async (env) => { @@ -58,7 +59,7 @@ export class DefaultSdkService implements SdkService { private userAgent: string = null, ) {} - userClient$(userId: UserId): Observable { + userClient$(userId: UserId): Observable | undefined> { // TODO: Figure out what happens when the user logs out if (this.sdkClientCache.has(userId)) { return this.sdkClientCache.get(userId); @@ -88,32 +89,31 @@ export class DefaultSdkService implements SdkService { // switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value. switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => { // Create our own observable to be able to implement clean-up logic - return new Observable((subscriber) => { - let client: BitwardenClient; - + return new Observable>((subscriber) => { const createAndInitializeClient = async () => { if (privateKey == null || userKey == null) { return undefined; } const settings = this.toSettings(env); - client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); + const client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys); return client; }; + let client: Rc; createAndInitializeClient() .then((c) => { - client = c; - subscriber.next(c); + client = c === undefined ? undefined : new Rc(c); + subscriber.next(client); }) .catch((e) => { subscriber.error(e); }); - return () => client?.free(); + return () => client?.markForDisposal(); }); }), tap({ diff --git a/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts new file mode 100644 index 00000000000..60dac4f21f1 --- /dev/null +++ b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts @@ -0,0 +1,7 @@ +import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; + +export class NoopSdkLoadService extends SdkLoadService { + async load() { + return; + } +} diff --git a/libs/common/src/platform/state/global-state.ts b/libs/common/src/platform/state/global-state.ts index b0f19c53faa..82a6e2b348c 100644 --- a/libs/common/src/platform/state/global-state.ts +++ b/libs/common/src/platform/state/global-state.ts @@ -9,7 +9,7 @@ import { StateUpdateOptions } from "./state-update-options"; export interface GlobalState { /** * Method for allowing you to manipulate state in an additive way. - * @param configureState callback for how you want manipulate this section of state + * @param configureState callback for how you want to manipulate this section of state * @param options Defaults given by @see {module:state-update-options#DEFAULT_OPTIONS} * @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null diff --git a/libs/common/src/platform/state/implementations/state-base.ts b/libs/common/src/platform/state/implementations/state-base.ts index 567de957e53..f285963d6e6 100644 --- a/libs/common/src/platform/state/implementations/state-base.ts +++ b/libs/common/src/platform/state/implementations/state-base.ts @@ -28,7 +28,7 @@ import { getStoredValue } from "./util"; // The parts of a KeyDefinition this class cares about to make it work type KeyDefinitionRequirements = { - deserializer: (jsonState: Jsonify) => T; + deserializer: (jsonState: Jsonify) => T | null; cleanupDelayMs: number; debug: Required; }; diff --git a/libs/common/src/platform/state/implementations/util.ts b/libs/common/src/platform/state/implementations/util.ts index f3d57fbafc4..0a9d76f6da5 100644 --- a/libs/common/src/platform/state/implementations/util.ts +++ b/libs/common/src/platform/state/implementations/util.ts @@ -5,12 +5,11 @@ import { AbstractStorageService } from "../../abstractions/storage.service"; export async function getStoredValue( key: string, storage: AbstractStorageService, - deserializer: (jsonValue: Jsonify) => T, + deserializer: (jsonValue: Jsonify) => T | null, ) { if (storage.valuesRequireDeserialization) { const jsonValue = await storage.get>(key); - const value = deserializer(jsonValue); - return value; + return deserializer(jsonValue); } else { const value = await storage.get(key); return value ?? null; diff --git a/libs/common/src/platform/state/key-definition.ts b/libs/common/src/platform/state/key-definition.ts index a270fc3e1a2..519e98ef52d 100644 --- a/libs/common/src/platform/state/key-definition.ts +++ b/libs/common/src/platform/state/key-definition.ts @@ -42,7 +42,7 @@ export type KeyDefinitionOptions = { * @param jsonValue The JSON object representation of your state. * @returns The fully typed version of your state. */ - readonly deserializer: (jsonValue: Jsonify) => T; + readonly deserializer: (jsonValue: Jsonify) => T | null; /** * The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed. * Defaults to 1000ms. diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index c83119d9ad4..c7901bc34e2 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -29,9 +29,20 @@ export const ORGANIZATION_MANAGEMENT_PREFERENCES_DISK = new StateDefinition( web: "disk-local", }, ); -export const AC_BANNERS_DISMISSED_DISK = new StateDefinition("acBannersDismissed", "disk", { - web: "disk-local", -}); +export const ACCOUNT_DEPROVISIONING_BANNER_DISK = new StateDefinition( + "showAccountDeprovisioningBanner", + "disk", + { + web: "disk-local", + }, +); +export const DELETE_MANAGED_USER_WARNING = new StateDefinition( + "showDeleteManagedUserWarning", + "disk", + { + web: "disk-local", + }, +); // Billing export const BILLING_DISK = new StateDefinition("billing", "disk"); diff --git a/libs/common/src/platform/state/user-state.ts b/libs/common/src/platform/state/user-state.ts index 44bc8732544..22c255eb985 100644 --- a/libs/common/src/platform/state/user-state.ts +++ b/libs/common/src/platform/state/user-state.ts @@ -16,6 +16,7 @@ export interface UserState { } export const activeMarker: unique symbol = Symbol("active"); + export interface ActiveUserState extends UserState { readonly [activeMarker]: true; @@ -32,7 +33,7 @@ export interface ActiveUserState extends UserState { * @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null * @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set. - + * * @returns A promise that must be awaited before your next action to ensure the update has been written to state. * Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state. */ @@ -41,6 +42,7 @@ export interface ActiveUserState extends UserState { options?: StateUpdateOptions, ) => Promise<[UserId, T]>; } + export interface SingleUserState extends UserState { readonly userId: UserId; @@ -51,7 +53,7 @@ export interface SingleUserState extends UserState { * @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null * @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set. - + * * @returns A promise that must be awaited before your next action to ensure the update has been written to state. * Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state. */ diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 138c7c03318..982be453457 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -197,6 +197,7 @@ export class DefaultSyncService extends CoreSyncService { await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor); await this.tokenService.setSecurityStamp(response.securityStamp, response.id); await this.accountService.setAccountEmailVerified(response.id, response.emailVerified); + await this.accountService.setAccountVerifyNewDeviceLogin(response.id, response.verifyDevices); await this.billingAccountProfileStateService.setHasPremium( response.premiumPersonally, diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index f40745142d0..ad59ad0837a 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -78,6 +78,7 @@ import { ApiKeyResponse } from "../auth/models/response/api-key.response"; import { AuthRequestResponse } from "../auth/models/response/auth-request.response"; import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response"; import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response"; +import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; @@ -157,6 +158,13 @@ export class ApiService implements ApiServiceAbstraction { private deviceType: string; private isWebClient = false; private isDesktopClient = false; + private refreshTokenPromise: Promise | undefined; + + /** + * The message (responseJson.ErrorModel.Message) that comes back from the server when a new device verification is required. + */ + private static readonly NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE = + "new device verification required"; constructor( private tokenService: TokenService, @@ -197,7 +205,12 @@ export class ApiService implements ApiServiceAbstraction { | PasswordTokenRequest | SsoTokenRequest | WebAuthnLoginTokenRequest, - ): Promise { + ): Promise< + | IdentityTokenResponse + | IdentityTwoFactorResponse + | IdentityCaptchaResponse + | IdentityDeviceVerificationResponse + > { const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", Accept: "application/json", @@ -245,6 +258,11 @@ export class ApiService implements ApiServiceAbstraction { Object.keys(responseJson.HCaptcha_SiteKey).length ) { return new IdentityCaptchaResponse(responseJson); + } else if ( + response.status === 400 && + responseJson?.ErrorModel?.Message === ApiService.NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE + ) { + return new IdentityDeviceVerificationResponse(responseJson); } } @@ -684,7 +702,7 @@ export class ApiService implements ApiServiceAbstraction { } deleteCipherAttachment(id: string, attachmentId: string): Promise { - return this.send("DELETE", "/ciphers/" + id + "/attachment/" + attachmentId, null, true, false); + return this.send("DELETE", "/ciphers/" + id + "/attachment/" + attachmentId, null, true, true); } deleteCipherAttachmentAdmin(id: string, attachmentId: string): Promise { @@ -1716,7 +1734,18 @@ export class ApiService implements ApiServiceAbstraction { ); } - protected async refreshToken(): Promise { + // Keep the running refreshTokenPromise to prevent parallel calls. + protected refreshToken(): Promise { + if (this.refreshTokenPromise === undefined) { + this.refreshTokenPromise = this.internalRefreshToken(); + void this.refreshTokenPromise.finally(() => { + this.refreshTokenPromise = undefined; + }); + } + return this.refreshTokenPromise; + } + + private async internalRefreshToken(): Promise { const refreshToken = await this.tokenService.getRefreshToken(); if (refreshToken != null && refreshToken !== "") { return this.refreshAccessToken(); diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index b06985e0ba7..da38ca5bfff 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -1,11 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map, from, zip, Observable } from "rxjs"; +import { firstValueFrom, map, from, zip } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service"; import { EventUploadService } from "../../abstractions/event/event-upload.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventType } from "../../enums"; @@ -17,8 +20,6 @@ import { CipherView } from "../../vault/models/view/cipher.view"; import { EVENT_COLLECTION } from "./key-definitions"; export class EventCollectionService implements EventCollectionServiceAbstraction { - private orgIds$: Observable; - constructor( private cipherService: CipherService, private stateProvider: StateProvider, @@ -26,11 +27,11 @@ export class EventCollectionService implements EventCollectionServiceAbstraction private eventUploadService: EventUploadService, private authService: AuthService, private accountService: AccountService, - ) { - this.orgIds$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []), - ); - } + ) {} + + private getOrgIds = (orgs: Organization[]): string[] => { + return orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []; + }; /** Adds an event to the active user's event collection * @param eventType the event type to be added @@ -42,14 +43,15 @@ export class EventCollectionService implements EventCollectionServiceAbstraction ciphers: CipherView[], uploadImmediately = false, ): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); if (!(await this.shouldUpdate(null, eventType, ciphers))) { return; } - const events$ = this.orgIds$.pipe( + const events$ = this.organizationService.organizations$(userId).pipe( + map((orgs) => this.getOrgIds(orgs)), map((orgs) => ciphers .filter((c) => orgs.includes(c.organizationId)) @@ -86,7 +88,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction uploadImmediately = false, organizationId: string = null, ): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); if (!(await this.shouldUpdate(organizationId, eventType, undefined, cipherId))) { @@ -122,8 +124,14 @@ export class EventCollectionService implements EventCollectionServiceAbstraction ): Promise { const cipher$ = from(this.cipherService.get(cipherId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + const orgIds$ = this.organizationService + .organizations$(userId) + .pipe(map((orgs) => this.getOrgIds(orgs))); + const [authStatus, orgIds, cipher] = await firstValueFrom( - zip(this.authService.activeAccountStatus$, this.orgIds$, cipher$), + zip(this.authService.activeAccountStatus$, orgIds$, cipher$), ); // The user must be authorized diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts deleted file mode 100644 index f88c904bee1..00000000000 --- a/libs/common/src/services/notifications.service.ts +++ /dev/null @@ -1,280 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import * as signalR from "@microsoft/signalr"; -import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack"; -import { firstValueFrom, Subscription } from "rxjs"; - -import { LogoutReason } from "@bitwarden/auth/common"; - -import { ApiService } from "../abstractions/api.service"; -import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service"; -import { AuthService } from "../auth/abstractions/auth.service"; -import { AuthenticationStatus } from "../auth/enums/authentication-status"; -import { NotificationType } from "../enums"; -import { - NotificationResponse, - SyncCipherNotification, - SyncFolderNotification, - SyncSendNotification, -} from "../models/response/notification.response"; -import { AppIdService } from "../platform/abstractions/app-id.service"; -import { EnvironmentService } from "../platform/abstractions/environment.service"; -import { LogService } from "../platform/abstractions/log.service"; -import { MessagingService } from "../platform/abstractions/messaging.service"; -import { StateService } from "../platform/abstractions/state.service"; -import { ScheduledTaskNames } from "../platform/scheduling/scheduled-task-name.enum"; -import { TaskSchedulerService } from "../platform/scheduling/task-scheduler.service"; -import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction"; - -export class NotificationsService implements NotificationsServiceAbstraction { - private signalrConnection: signalR.HubConnection; - private url: string; - private connected = false; - private inited = false; - private inactive = false; - private reconnectTimerSubscription: Subscription; - private isSyncingOnReconnect = true; - - constructor( - private logService: LogService, - private syncService: SyncService, - private appIdService: AppIdService, - private apiService: ApiService, - private environmentService: EnvironmentService, - private logoutCallback: (logoutReason: LogoutReason) => Promise, - private stateService: StateService, - private authService: AuthService, - private messagingService: MessagingService, - private taskSchedulerService: TaskSchedulerService, - ) { - this.taskSchedulerService.registerTaskHandler( - ScheduledTaskNames.notificationsReconnectTimeout, - () => this.reconnect(this.isSyncingOnReconnect), - ); - this.environmentService.environment$.subscribe(() => { - if (!this.inited) { - return; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.init(); - }); - } - - async init(): Promise { - this.inited = false; - this.url = (await firstValueFrom(this.environmentService.environment$)).getNotificationsUrl(); - - // Set notifications server URL to `https://-` to effectively disable communication - // with the notifications server from the client app - if (this.url === "https://-") { - return; - } - - if (this.signalrConnection != null) { - this.signalrConnection.off("ReceiveMessage"); - this.signalrConnection.off("Heartbeat"); - await this.signalrConnection.stop(); - this.connected = false; - this.signalrConnection = null; - } - - this.signalrConnection = new signalR.HubConnectionBuilder() - .withUrl(this.url + "/hub", { - accessTokenFactory: () => this.apiService.getActiveBearerToken(), - skipNegotiation: true, - transport: signalR.HttpTransportType.WebSockets, - }) - .withHubProtocol(new signalRMsgPack.MessagePackHubProtocol() as signalR.IHubProtocol) - // .configureLogging(signalR.LogLevel.Trace) - .build(); - - this.signalrConnection.on("ReceiveMessage", (data: any) => - this.processNotification(new NotificationResponse(data)), - ); - // eslint-disable-next-line - this.signalrConnection.on("Heartbeat", (data: any) => { - /*console.log('Heartbeat!');*/ - }); - this.signalrConnection.onclose(() => { - this.connected = false; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.reconnect(true); - }); - this.inited = true; - if (await this.isAuthedAndUnlocked()) { - await this.reconnect(false); - } - } - - async updateConnection(sync = false): Promise { - if (!this.inited) { - return; - } - try { - if (await this.isAuthedAndUnlocked()) { - await this.reconnect(sync); - } else { - await this.signalrConnection.stop(); - } - } catch (e) { - this.logService.error(e.toString()); - } - } - - async reconnectFromActivity(): Promise { - this.inactive = false; - if (this.inited && !this.connected) { - await this.reconnect(true); - } - } - - async disconnectFromInactivity(): Promise { - this.inactive = true; - if (this.inited && this.connected) { - await this.signalrConnection.stop(); - } - } - - private async processNotification(notification: NotificationResponse) { - const appId = await this.appIdService.getAppId(); - if (notification == null || notification.contextId === appId) { - return; - } - - const isAuthenticated = await this.stateService.getIsAuthenticated(); - const payloadUserId = notification.payload.userId || notification.payload.UserId; - const myUserId = await this.stateService.getUserId(); - if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) { - return; - } - - switch (notification.type) { - case NotificationType.SyncCipherCreate: - case NotificationType.SyncCipherUpdate: - await this.syncService.syncUpsertCipher( - notification.payload as SyncCipherNotification, - notification.type === NotificationType.SyncCipherUpdate, - ); - break; - case NotificationType.SyncCipherDelete: - case NotificationType.SyncLoginDelete: - await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); - break; - case NotificationType.SyncFolderCreate: - case NotificationType.SyncFolderUpdate: - await this.syncService.syncUpsertFolder( - notification.payload as SyncFolderNotification, - notification.type === NotificationType.SyncFolderUpdate, - payloadUserId, - ); - break; - case NotificationType.SyncFolderDelete: - await this.syncService.syncDeleteFolder( - notification.payload as SyncFolderNotification, - payloadUserId, - ); - break; - case NotificationType.SyncVault: - case NotificationType.SyncCiphers: - case NotificationType.SyncSettings: - if (isAuthenticated) { - await this.syncService.fullSync(false); - } - break; - case NotificationType.SyncOrganizations: - if (isAuthenticated) { - // An organization update may not have bumped the user's account revision date, so force a sync - await this.syncService.fullSync(true); - } - break; - case NotificationType.SyncOrgKeys: - if (isAuthenticated) { - await this.syncService.fullSync(true); - // Stop so a reconnect can be made - await this.signalrConnection.stop(); - } - break; - case NotificationType.LogOut: - if (isAuthenticated) { - this.logService.info("[Notifications Service] Received logout notification"); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.logoutCallback("logoutNotification"); - } - break; - case NotificationType.SyncSendCreate: - case NotificationType.SyncSendUpdate: - await this.syncService.syncUpsertSend( - notification.payload as SyncSendNotification, - notification.type === NotificationType.SyncSendUpdate, - ); - break; - case NotificationType.SyncSendDelete: - await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification); - break; - case NotificationType.AuthRequest: - { - this.messagingService.send("openLoginApproval", { - notificationId: notification.payload.id, - }); - } - break; - case NotificationType.SyncOrganizationStatusChanged: - if (isAuthenticated) { - await this.syncService.fullSync(true); - } - break; - case NotificationType.SyncOrganizationCollectionSettingChanged: - if (isAuthenticated) { - await this.syncService.fullSync(true); - } - break; - default: - break; - } - } - - private async reconnect(sync: boolean) { - this.reconnectTimerSubscription?.unsubscribe(); - - if (this.connected || !this.inited || this.inactive) { - return; - } - const authedAndUnlocked = await this.isAuthedAndUnlocked(); - if (!authedAndUnlocked) { - return; - } - - try { - await this.signalrConnection.start(); - this.connected = true; - if (sync) { - await this.syncService.fullSync(false); - } - } catch (e) { - this.logService.error(e); - } - - if (!this.connected) { - this.isSyncingOnReconnect = sync; - this.reconnectTimerSubscription = this.taskSchedulerService.setTimeout( - ScheduledTaskNames.notificationsReconnectTimeout, - this.random(120000, 300000), - ); - } - } - - private async isAuthedAndUnlocked() { - const authStatus = await this.authService.getAuthStatus(); - return authStatus >= AuthenticationStatus.Unlocked; - } - - private random(min: number, max: number) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1)) + min; - } -} diff --git a/libs/common/src/state-migrations/.eslintrc.json b/libs/common/src/state-migrations/.eslintrc.json deleted file mode 100644 index 4b66f0a32fa..00000000000 --- a/libs/common/src/state-migrations/.eslintrc.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "overrides": [ - { - "files": ["*"], - "rules": { - "import/no-restricted-paths": [ - "error", - { - "basePath": "libs/common/src/state-migrations", - "zones": [ - { - "target": "./", - "from": "../", - // Relative to from, not basePath - "except": ["state-migrations"], - "message": "State migrations should rarely import from the greater codebase. If you need to import from another location, take into account the likelihood of change in that code and consider copying to the migration instead." - } - ] - } - ] - } - } - ] -} diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 81b1016a53d..169de447f10 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -66,13 +66,14 @@ import { ForwarderOptionsMigrator } from "./migrations/65-migrate-forwarder-sett import { MoveFinalDesktopSettingsMigrator } from "./migrations/66-move-final-desktop-settings"; import { RemoveUnassignedItemsBannerDismissed } from "./migrations/67-remove-unassigned-items-banner-dismissed"; import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date"; +import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 68; +export const CURRENT_VERSION = 69; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -142,7 +143,8 @@ export function createMigrationBuilder() { .with(ForwarderOptionsMigrator, 64, 65) .with(MoveFinalDesktopSettingsMigrator, 65, 66) .with(RemoveUnassignedItemsBannerDismissed, 66, 67) - .with(MoveLastSyncDate, 67, CURRENT_VERSION); + .with(MoveLastSyncDate, 67, 68) + .with(MigrateIncorrectFolderKey, 68, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts new file mode 100644 index 00000000000..e5dec943f78 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.spec.ts @@ -0,0 +1,98 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { MigrateIncorrectFolderKey } from "./69-migrate-incorrect-folder-key"; + +function exampleJSON() { + return { + global_account_accounts: { + user1: null as any, + user2: null as any, + }, + user_user1_folder_folder: { + // Incorrect "folder" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + user_user2_folder_folder: null as any, + }; +} + +describe("MigrateIncorrectFolderKey", () => { + const sut = new MigrateIncorrectFolderKey(68, 69); + it("migrates data", async () => { + const output = await runMigrator(sut, exampleJSON()); + + expect(output).toEqual({ + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folders: { + // Correct "folders" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }); + }); + + it("rolls back data", async () => { + const output = await runMigrator( + sut, + { + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folders: { + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }, + "rollback", + ); + + expect(output).toEqual({ + global_account_accounts: { + user1: null, + user2: null, + }, + user_user1_folder_folder: { + // Incorrect "folder" key + folderId1: { + id: "folderId1", + name: "folder-name-1", + revisionDate: "folder-revision-date-1", + }, + folderId2: { + id: "folderId2", + name: "folder-name-2", + revisionDate: "folder-revision-date-2", + }, + }, + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts new file mode 100644 index 00000000000..046c0cf0dfa --- /dev/null +++ b/libs/common/src/state-migrations/migrations/69-migrate-incorrect-folder-key.ts @@ -0,0 +1,45 @@ +import { + KeyDefinitionLike, + MigrationHelper, +} from "@bitwarden/common/state-migrations/migration-helper"; +import { Migrator } from "@bitwarden/common/state-migrations/migrator"; + +const BAD_FOLDER_KEY: KeyDefinitionLike = { + key: "folder", // We inadvertently changed the key from "folders" to "folder" + stateDefinition: { + name: "folder", + }, +}; + +const GOOD_FOLDER_KEY: KeyDefinitionLike = { + key: "folders", // We should keep the key as "folders" + stateDefinition: { + name: "folder", + }, +}; + +export class MigrateIncorrectFolderKey extends Migrator<68, 69> { + async migrate(helper: MigrationHelper): Promise { + async function migrateUser(userId: string) { + const value = await helper.getFromUser(userId, BAD_FOLDER_KEY); + if (value != null) { + await helper.setToUser(userId, GOOD_FOLDER_KEY, value); + } + await helper.removeFromUser(userId, BAD_FOLDER_KEY); + } + const users = await helper.getKnownUserIds(); + await Promise.all(users.map((userId) => migrateUser(userId))); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackUser(userId: string) { + const value = await helper.getFromUser(userId, GOOD_FOLDER_KEY); + if (value != null) { + await helper.setToUser(userId, BAD_FOLDER_KEY, value); + } + await helper.removeFromUser(userId, GOOD_FOLDER_KEY); + } + const users = await helper.getKnownUserIds(); + await Promise.all(users.map((userId) => rollbackUser(userId))); + } +} diff --git a/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.spec.ts b/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.spec.ts new file mode 100644 index 00000000000..59f39d195e9 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.spec.ts @@ -0,0 +1,50 @@ +import { runMigrator } from "../migration-helper.spec"; +import { IRREVERSIBLE } from "../migrator"; + +import { RemoveAcBannersDismissed } from "./70-remove-ac-banner-dismissed"; + +describe("RemoveAcBannersDismissed", () => { + const sut = new RemoveAcBannersDismissed(69, 70); + + describe("migrate", () => { + it("deletes ac banner from all users", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + user_user1_showProviderClientVaultPrivacyBanner_acBannersDismissed: true, + user_user2_showProviderClientVaultPrivacyBanner_acBannersDismissed: true, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + }); + }); + }); + + describe("rollback", () => { + it("is irreversible", async () => { + await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.ts b/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.ts new file mode 100644 index 00000000000..087994b508f --- /dev/null +++ b/libs/common/src/state-migrations/migrations/70-remove-ac-banner-dismissed.ts @@ -0,0 +1,23 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +export const SHOW_BANNER_KEY: KeyDefinitionLike = { + key: "acBannersDismissed", + stateDefinition: { name: "showProviderClientVaultPrivacyBanner" }, +}; + +export class RemoveAcBannersDismissed extends Migrator<69, 70> { + async migrate(helper: MigrationHelper): Promise { + await Promise.all( + (await helper.getAccounts()).map(async ({ userId }) => { + if (helper.getFromUser(userId, SHOW_BANNER_KEY) != null) { + await helper.removeFromUser(userId, SHOW_BANNER_KEY); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +} diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts index 0b60aef4917..66edc5a4838 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts @@ -3,7 +3,7 @@ import { BehaviorSubject, Subject } from "rxjs"; import { KeyService } from "@bitwarden/key-management"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "../../types/csprng"; import { OrganizationId, UserId } from "../../types/guid"; diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts index d4a8dec7dc3..c91181a004a 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts @@ -14,7 +14,7 @@ import { import { KeyService } from "@bitwarden/key-management"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { OrganizationId, UserId } from "../../types/guid"; import { OrganizationBound, diff --git a/libs/common/src/tools/cryptography/organization-key-encryptor.spec.ts b/libs/common/src/tools/cryptography/organization-key-encryptor.spec.ts index 62c8ea24ae6..3d93db81389 100644 --- a/libs/common/src/tools/cryptography/organization-key-encryptor.spec.ts +++ b/libs/common/src/tools/cryptography/organization-key-encryptor.spec.ts @@ -1,6 +1,6 @@ import { mock } from "jest-mock-extended"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "../../types/csprng"; diff --git a/libs/common/src/tools/cryptography/organization-key-encryptor.ts b/libs/common/src/tools/cryptography/organization-key-encryptor.ts index d3b7dae10f5..31f3db91232 100644 --- a/libs/common/src/tools/cryptography/organization-key-encryptor.ts +++ b/libs/common/src/tools/cryptography/organization-key-encryptor.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { OrganizationId } from "../../types/guid"; import { OrgKey } from "../../types/key"; diff --git a/libs/common/src/tools/cryptography/user-key-encryptor.spec.ts b/libs/common/src/tools/cryptography/user-key-encryptor.spec.ts index 5b0ee5103cb..e52190500b0 100644 --- a/libs/common/src/tools/cryptography/user-key-encryptor.spec.ts +++ b/libs/common/src/tools/cryptography/user-key-encryptor.spec.ts @@ -1,6 +1,6 @@ import { mock } from "jest-mock-extended"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "../../types/csprng"; diff --git a/libs/common/src/tools/cryptography/user-key-encryptor.ts b/libs/common/src/tools/cryptography/user-key-encryptor.ts index 296c33ea1dc..4b7cd1516a0 100644 --- a/libs/common/src/tools/cryptography/user-key-encryptor.ts +++ b/libs/common/src/tools/cryptography/user-key-encryptor.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { UserId } from "../../types/guid"; import { UserKey } from "../../types/key"; diff --git a/libs/common/src/tools/dependencies.ts b/libs/common/src/tools/dependencies.ts index c22e71cff67..befe1ca5406 100644 --- a/libs/common/src/tools/dependencies.ts +++ b/libs/common/src/tools/dependencies.ts @@ -1,7 +1,7 @@ import { Observable } from "rxjs"; -import { Policy } from "../admin-console/models/domain/policy"; -import { OrganizationId, UserId } from "../types/guid"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrganizationEncryptor } from "./cryptography/organization-encryptor.abstraction"; import { UserEncryptor } from "./cryptography/user-encryptor.abstraction"; @@ -152,7 +152,8 @@ export type SingleUserDependency = { }; /** A pattern for types that emit values exclusively when the dependency - * emits a message. + * emits a message. Set a type parameter when your method requires contextual + * information when the request is issued. * * Consumers of this dependency should emit when `on$` emits. If `on$` * completes, the consumer should also complete. If `on$` @@ -161,10 +162,10 @@ export type SingleUserDependency = { * @remarks This dependency is useful when you have a nondeterministic * or stateful algorithm that you would like to run when an event occurs. */ -export type OnDependency = { +export type OnDependency = { /** The stream that controls emissions */ - on$: Observable; + on$: Observable; }; /** A pattern for types that emit when a dependency is `true`. diff --git a/libs/common/src/tools/send/models/domain/send.spec.ts b/libs/common/src/tools/send/models/domain/send.spec.ts index fcc273d41bb..79f6c03adc8 100644 --- a/libs/common/src/tools/send/models/domain/send.spec.ts +++ b/libs/common/src/tools/send/models/domain/send.spec.ts @@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended"; import { KeyService } from "@bitwarden/key-management"; import { makeStaticByteArray, mockEnc } from "../../../../../spec"; -import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../../key-management/crypto/abstractions/encrypt.service"; import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../../platform/services/container.service"; import { UserKey } from "../../../../types/key"; diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 662fee02bbf..26cc0a46708 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -10,7 +10,7 @@ import { awaitAsync, mockAccountServiceWith, } from "../../../../spec"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EnvironmentService } from "../../../platform/abstractions/environment.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 7021c942d44..1b5e5f6aa31 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -4,7 +4,7 @@ import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from import { PBKDF2KdfConfig, KeyService } from "@bitwarden/key-management"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { Utils } from "../../../platform/misc/utils"; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 88cd476606d..0672ae29e91 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -93,7 +93,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; + ) => Promise; shareManyWithServer: ( ciphers: CipherView[], organizationId: string, @@ -154,8 +154,8 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; deleteWithServer: (id: string, asAdmin?: boolean) => Promise; deleteManyWithServer: (ids: string[], asAdmin?: boolean) => Promise; - deleteAttachment: (id: string, attachmentId: string) => Promise; - deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise; + deleteAttachment: (id: string, revisionDate: string, attachmentId: string) => Promise; + deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise; sortCiphersByLastUsed: (a: CipherView, b: CipherView) => number; sortCiphersByLastUsedThenName: (a: CipherView, b: CipherView) => number; getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number; diff --git a/libs/common/src/vault/icon/build-cipher-icon.spec.ts b/libs/common/src/vault/icon/build-cipher-icon.spec.ts new file mode 100644 index 00000000000..8de65390bf7 --- /dev/null +++ b/libs/common/src/vault/icon/build-cipher-icon.spec.ts @@ -0,0 +1,141 @@ +import { CipherType } from "../enums"; +import { CipherView } from "../models/view/cipher.view"; + +import { buildCipherIcon } from "./build-cipher-icon"; + +describe("buildCipherIcon", () => { + const iconServerUrl = "https://icons.example"; + describe("Login cipher", () => { + const cipher = { + type: CipherType.Login, + login: { + uri: "https://test.example", + }, + } as any as CipherView; + + it.each([true, false])("handles android app URIs for showFavicon setting %s", (showFavicon) => { + setUri("androidapp://test.example"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon); + + expect(iconDetails).toEqual({ + icon: "bwi-android", + image: null, + fallbackImage: "", + imageEnabled: showFavicon, + }); + }); + + it("does not mark as an android app if the protocol is not androidapp", () => { + // This weird URI points to test.androidapp with a default port and path of /.example + setUri("https://test.androidapp://.example"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: "https://icons.example/test.androidapp/icon.png", + fallbackImage: "images/bwi-globe.png", + imageEnabled: true, + }); + }); + + it.each([true, false])("handles ios app URIs for showFavicon setting %s", (showFavicon) => { + setUri("iosapp://test.example"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon); + + expect(iconDetails).toEqual({ + icon: "bwi-apple", + image: null, + fallbackImage: "", + imageEnabled: showFavicon, + }); + }); + + it("does not mark as an ios app if the protocol is not iosapp", () => { + // This weird URI points to test.iosapp with a default port and path of /.example + setUri("https://test.iosapp://.example"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: "https://icons.example/test.iosapp/icon.png", + fallbackImage: "images/bwi-globe.png", + imageEnabled: true, + }); + }); + + const testUris = ["test.example", "https://test.example"]; + + it.each(testUris)("resolves favicon for %s", (uri) => { + setUri(uri); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: "https://icons.example/test.example/icon.png", + fallbackImage: "images/bwi-globe.png", + imageEnabled: true, + }); + }); + + it.each(testUris)("does not resolve favicon for %s if showFavicon is false", () => { + setUri("https://test.example"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, false); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: undefined, + fallbackImage: "", + imageEnabled: false, + }); + }); + + it("does not resolve a favicon if the URI is missing a `.`", () => { + setUri("test"); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: undefined, + fallbackImage: "", + imageEnabled: true, + }); + }); + + it.each(["test.onion", "test.i2p"])("does not resolve a favicon for %s", (uri) => { + setUri(`https://${uri}`); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: null, + fallbackImage: "images/bwi-globe.png", + imageEnabled: true, + }); + }); + + it.each([null, undefined])("does not resolve a favicon if there is no uri", (nullish) => { + setUri(nullish as any as string); + + const iconDetails = buildCipherIcon(iconServerUrl, cipher, true); + + expect(iconDetails).toEqual({ + icon: "bwi-globe", + image: null, + fallbackImage: "", + imageEnabled: true, + }); + }); + + function setUri(uri: string) { + (cipher.login as { uri: string }).uri = uri; + } + }); +}); diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 8cae7170738..d1ee1dcc1ef 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -1,10 +1,9 @@ import { mock, MockProxy } from "jest-mock-extended"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../platform/services/container.service"; diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index dd79da3086e..9eadd20f543 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -1,12 +1,11 @@ import { mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { ContainerService } from "../../../platform/services/container.service"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; diff --git a/libs/common/src/vault/models/domain/folder.spec.ts b/libs/common/src/vault/models/domain/folder.spec.ts index 785852b884e..ff1c38bdd45 100644 --- a/libs/common/src/vault/models/domain/folder.spec.ts +++ b/libs/common/src/vault/models/domain/folder.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { makeEncString, makeSymmetricCryptoKey, mockEnc, mockFromJson } from "../../../../spec"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; diff --git a/libs/common/src/vault/models/domain/folder.ts b/libs/common/src/vault/models/domain/folder.ts index 93d04607af5..65018e3cf08 100644 --- a/libs/common/src/vault/models/domain/folder.ts +++ b/libs/common/src/vault/models/domain/folder.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; diff --git a/libs/common/src/vault/models/domain/login-uri.spec.ts b/libs/common/src/vault/models/domain/login-uri.spec.ts index a1ecb473597..6346f38f0de 100644 --- a/libs/common/src/vault/models/domain/login-uri.spec.ts +++ b/libs/common/src/vault/models/domain/login-uri.spec.ts @@ -2,8 +2,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; import { mockEnc, mockFromJson } from "../../../../spec"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { LoginUriData } from "../data/login-uri.data"; diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 20dbd23065c..650a1e9dc45 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -142,6 +142,13 @@ export class CipherView implements View, InitializerMetadata { ); } + get canAssignToCollections(): boolean { + if (this.organizationId == null) { + return true; + } + + return this.edit && this.viewPassword; + } /** * Determines if the cipher can be launched in a new browser tab. */ diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts index 15e8b03bfad..37ddfdeaeeb 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.spec.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -1,11 +1,13 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; +import { Observable, firstValueFrom, of } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "../../admin-console/models/domain/organization"; -import { CollectionId } from "../../types/guid"; +import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; import { CipherView } from "../models/view/cipher.view"; import { @@ -18,6 +20,8 @@ describe("CipherAuthorizationService", () => { const mockCollectionService = mock(); const mockOrganizationService = mock(); + const mockUserId = Utils.newGuid() as UserId; + let mockAccountService: FakeAccountService; // Mock factories const createMockCipher = ( @@ -42,6 +46,7 @@ describe("CipherAuthorizationService", () => { isAdmin = false, editAnyCollection = false, } = {}) => ({ + id: "org1", allowAdminAccessToAllCollectionItems, canEditAllCiphers, canEditUnassignedCiphers, @@ -53,9 +58,11 @@ describe("CipherAuthorizationService", () => { beforeEach(() => { jest.clearAllMocks(); + mockAccountService = mockAccountServiceWith(mockUserId); cipherAuthorizationService = new DefaultCipherAuthorizationService( mockCollectionService, mockOrganizationService, + mockAccountService, ); }); @@ -72,7 +79,9 @@ describe("CipherAuthorizationService", () => { it("should return true if isAdminConsoleAction is true and cipher is unassigned", (done) => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ canEditUnassignedCiphers: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization]) as Observable, + ); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(true); @@ -83,11 +92,13 @@ describe("CipherAuthorizationService", () => { it("should return true if isAdminConsoleAction is true and user can edit all ciphers in the org", (done) => { const cipher = createMockCipher("org1", ["col1"]) as CipherView; const organization = createMockOrganization({ canEditAllCiphers: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization]) as Observable, + ); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(true); - expect(mockOrganizationService.get$).toHaveBeenCalledWith("org1"); + expect(mockOrganizationService.organizations$).toHaveBeenCalledWith(mockUserId); done(); }); }); @@ -95,7 +106,7 @@ describe("CipherAuthorizationService", () => { it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ canEditUnassignedCiphers: false }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(false); @@ -106,8 +117,8 @@ describe("CipherAuthorizationService", () => { it("should return true if activeCollectionId is provided and has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const activeCollectionId = "col1" as CollectionId; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", true), @@ -132,8 +143,8 @@ describe("CipherAuthorizationService", () => { it("should return false if activeCollectionId is provided and manage permission is not present", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const activeCollectionId = "col1" as CollectionId; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -157,8 +168,8 @@ describe("CipherAuthorizationService", () => { it("should return true if any collection has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2", "col3"]) as CipherView; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -182,8 +193,8 @@ describe("CipherAuthorizationService", () => { it("should return false if no collection has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -216,7 +227,9 @@ describe("CipherAuthorizationService", () => { it("should return true for admin users", async () => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ isAdmin: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const result = await firstValueFrom( cipherAuthorizationService.canCloneCipher$(cipher, true), @@ -227,7 +240,9 @@ describe("CipherAuthorizationService", () => { it("should return true for custom user with canEditAnyCollection", async () => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ editAnyCollection: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const result = await firstValueFrom( cipherAuthorizationService.canCloneCipher$(cipher, true), @@ -240,7 +255,9 @@ describe("CipherAuthorizationService", () => { it("should return true if at least one cipher collection has manage permission", async () => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const organization = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const allCollections = [ createMockCollection("col1", true), @@ -257,7 +274,9 @@ describe("CipherAuthorizationService", () => { it("should return false if no collection has manage permission", async () => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const organization = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const allCollections = [ createMockCollection("col1", false), diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts index 025d6b1cdc3..fbee3ed8622 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -3,9 +3,10 @@ import { map, Observable, of, shareReplay, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { CollectionId } from "@bitwarden/common/types/guid"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { CollectionId } from "../../types/guid"; import { Cipher } from "../models/domain/cipher"; import { CipherView } from "../models/view/cipher.view"; @@ -51,8 +52,14 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer constructor( private collectionService: CollectionService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} + private organization$ = (cipher: CipherLike) => + this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + map((orgs) => orgs.find((org) => org.id === cipher.organizationId)), + ); /** * * {@link CipherAuthorizationService.canDeleteCipher$} @@ -66,7 +73,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.organizationService.get$(cipher.organizationId).pipe( + return this.organization$(cipher).pipe( switchMap((organization) => { if (isAdminConsoleAction) { // If the user is an admin, they can delete an unassigned cipher @@ -104,7 +111,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.organizationService.get$(cipher.organizationId).pipe( + return this.organization$(cipher).pipe( switchMap((organization) => { // Admins and custom users can always clone when in the Admin Console if ( diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 0d6578f165d..c59f6672985 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,12 +1,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, map, of } from "rxjs"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { - CipherDecryptionKeys, - KeyService, -} from "../../../../key-management/src/abstractions/key.service"; +import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management"; + import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { makeStaticByteArray } from "../../../spec/utils"; @@ -14,10 +10,10 @@ import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; +import { BulkEncryptService } from "../../key-management/crypto/abstractions/bulk-encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { UriMatchStrategy } from "../../models/domain/domain-service"; -import { BulkEncryptService } from "../../platform/abstractions/bulk-encrypt.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index fe946fbb064..9e06d3335c3 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -14,22 +14,21 @@ import { } from "rxjs"; import { SemVer } from "semver"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { FeatureFlag } from "../../enums/feature-flag.enum"; +import { BulkEncryptService } from "../../key-management/crypto/abstractions/bulk-encrypt.service"; +import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; -import { BulkEncryptService } from "../../platform/abstractions/bulk-encrypt.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; import { sequentialize } from "../../platform/misc/sequentialize"; @@ -786,7 +785,7 @@ export class CipherService implements CipherServiceAbstraction { organizationId: string, collectionIds: string[], userId: UserId, - ): Promise { + ): Promise { const attachmentPromises: Promise[] = []; if (cipher.attachments != null) { cipher.attachments.forEach((attachment) => { @@ -806,6 +805,7 @@ export class CipherService implements CipherServiceAbstraction { const response = await this.apiService.putShareCipher(cipher.id, request); const data = new CipherData(response, collectionIds); await this.upsert(data); + return new Cipher(data, cipher.localData); } async shareManyWithServer( @@ -1077,7 +1077,11 @@ export class CipherService implements CipherServiceAbstraction { await this.delete(ids); } - async deleteAttachment(id: string, attachmentId: string): Promise { + async deleteAttachment( + id: string, + revisionDate: string, + attachmentId: string, + ): Promise { let ciphers = await firstValueFrom(this.ciphers$); const cipherId = id as CipherId; // eslint-disable-next-line @@ -1091,6 +1095,10 @@ export class CipherService implements CipherServiceAbstraction { } } + // Deleting the cipher updates the revision date on the server, + // Update the stored `revisionDate` to match + ciphers[cipherId].revisionDate = revisionDate; + await this.clearCache(); await this.encryptedCiphersState.update(() => { if (ciphers == null) { @@ -1098,15 +1106,20 @@ export class CipherService implements CipherServiceAbstraction { } return ciphers; }); + + return ciphers[cipherId]; } - async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { + async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { + let cipherResponse = null; try { - await this.apiService.deleteCipherAttachment(id, attachmentId); + cipherResponse = await this.apiService.deleteCipherAttachment(id, attachmentId); } catch (e) { return Promise.reject((e as ErrorResponse).getSingleMessage()); } - await this.deleteAttachment(id, attachmentId); + const cipherData = CipherData.fromJSON(cipherResponse?.cipher); + + return await this.deleteAttachment(id, cipherData.revisionDate, attachmentId); } sortCiphersByLastUsed(a: CipherView, b: CipherView): number { diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index cc3aa1946ca..ced4e2dceb7 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -1,14 +1,13 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + import { makeEncString } from "../../../../spec"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service"; import { FakeSingleUserState } from "../../../../spec/fake-state"; import { FakeStateProvider } from "../../../../spec/fake-state-provider"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { Utils } from "../../../platform/misc/utils"; import { EncString } from "../../../platform/models/domain/enc-string"; diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index c21a92fd894..3d272416fbe 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -2,12 +2,11 @@ // @ts-strict-ignore import { Observable, Subject, firstValueFrom, map, shareReplay, switchMap, merge } from "rxjs"; -import { EncryptService } from ".././../../platform/abstractions/encrypt.service"; -import { Utils } from ".././../../platform/misc/utils"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; +import { KeyService } from "@bitwarden/key-management"; + +import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; +import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; diff --git a/libs/common/src/vault/services/key-state/folder.state.ts b/libs/common/src/vault/services/key-state/folder.state.ts index 99ad8e5ae35..b3e61f5bf31 100644 --- a/libs/common/src/vault/services/key-state/folder.state.ts +++ b/libs/common/src/vault/services/key-state/folder.state.ts @@ -6,7 +6,7 @@ import { FolderView } from "../../models/view/folder.view"; export const FOLDER_ENCRYPTED_FOLDERS = UserKeyDefinition.record( FOLDER_DISK, - "folder", + "folders", { deserializer: (obj: Jsonify) => FolderData.fromJSON(obj), clearOn: ["logout"], diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index 76ff702e88b..0e3dbd6f1b9 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf, NgClass } from "@angular/common"; +import { NgClass } from "@angular/common"; import { Component, Input, OnChanges } from "@angular/core"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; @@ -18,9 +18,11 @@ const SizeClasses: Record = { @Component({ selector: "bit-avatar", - template: ``, + template: `@if (src) { + + }`, standalone: true, - imports: [NgIf, NgClass], + imports: [NgClass], }) export class AvatarComponent implements OnChanges { @Input() border = false; diff --git a/libs/components/src/badge-list/badge-list.component.html b/libs/components/src/badge-list/badge-list.component.html index ebd63117d03..c8aa7b84680 100644 --- a/libs/components/src/badge-list/badge-list.component.html +++ b/libs/components/src/badge-list/badge-list.component.html @@ -1,11 +1,15 @@

- + @for (item of filteredItems; track item; let last = $last) { {{ item }} - , - - - {{ "plusNMore" | i18n: (items.length - filteredItems.length).toString() }} - + @if (!last || isFiltered) { + , + } + } + @if (isFiltered) { + + {{ "plusNMore" | i18n: (items.length - filteredItems.length).toString() }} + + }
diff --git a/libs/components/src/badge-list/badge-list.component.ts b/libs/components/src/badge-list/badge-list.component.ts index 7d152761ed0..86e9a84cb77 100644 --- a/libs/components/src/badge-list/badge-list.component.ts +++ b/libs/components/src/badge-list/badge-list.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CommonModule } from "@angular/common"; + import { Component, Input, OnChanges } from "@angular/core"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -11,7 +11,7 @@ import { BadgeModule, BadgeVariant } from "../badge"; selector: "bit-badge-list", templateUrl: "badge-list.component.html", standalone: true, - imports: [CommonModule, BadgeModule, I18nPipe], + imports: [BadgeModule, I18nPipe], }) export class BadgeListComponent implements OnChanges { private _maxItems: number; diff --git a/libs/components/src/banner/banner.component.html b/libs/components/src/banner/banner.component.html index 566494eb64a..1a9d58d342a 100644 --- a/libs/components/src/banner/banner.component.html +++ b/libs/components/src/banner/banner.component.html @@ -4,21 +4,24 @@ [attr.role]="useAlertRole ? 'status' : null" [attr.aria-live]="useAlertRole ? 'polite' : null" > - + @if (icon) { + + } - + @if (showClose) { + + }
diff --git a/libs/components/src/breadcrumbs/breadcrumb.component.html b/libs/components/src/breadcrumbs/breadcrumb.component.html index dd5bac9beb4..bb4dc7cdffe 100644 --- a/libs/components/src/breadcrumbs/breadcrumb.component.html +++ b/libs/components/src/breadcrumbs/breadcrumb.component.html @@ -1,3 +1,6 @@ - + @if (icon) { + + } + diff --git a/libs/components/src/breadcrumbs/breadcrumb.component.ts b/libs/components/src/breadcrumbs/breadcrumb.component.ts index ce18bde171f..53c46a9b24a 100644 --- a/libs/components/src/breadcrumbs/breadcrumb.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumb.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf } from "@angular/common"; + import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core"; import { QueryParamsHandling } from "@angular/router"; @@ -8,7 +8,6 @@ import { QueryParamsHandling } from "@angular/router"; selector: "bit-breadcrumb", templateUrl: "./breadcrumb.component.html", standalone: true, - imports: [NgIf], }) export class BreadcrumbComponent { @Input() diff --git a/libs/components/src/breadcrumbs/breadcrumbs.component.html b/libs/components/src/breadcrumbs/breadcrumbs.component.html index 502bb0bb8e7..5205e19cee5 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.component.html +++ b/libs/components/src/breadcrumbs/breadcrumbs.component.html @@ -1,5 +1,5 @@ - - +@for (breadcrumb of beforeOverflow; track breadcrumb; let last = $last) { + @if (breadcrumb.route) { - - + } + @if (!breadcrumb.route) { - - - - - - + } + @if (!last) { + + } +} +@if (hasOverflow) { + @if (beforeOverflow.length > 0) { + + } - - - + @for (breadcrumb of overflow; track breadcrumb) { + @if (breadcrumb.route) { - - + } + @if (!breadcrumb.route) { - - + } + } - - - + @for (breadcrumb of afterOverflow; track breadcrumb; let last = $last) { + @if (breadcrumb.route) { - - + } + @if (!breadcrumb.route) { - - - - + } + @if (!last) { + + } + } +} diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 469c2d1b51b..6024b0559f2 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -86,16 +86,15 @@ export const DisabledWithAttribute: Story = { render: (args) => ({ props: args, template: ` - + @if (disabled) { - - + } @else { - + } `, }), args: { diff --git a/libs/components/src/callout/callout.component.html b/libs/components/src/callout/callout.component.html index f64e197b9ef..bb7f918df32 100644 --- a/libs/components/src/callout/callout.component.html +++ b/libs/components/src/callout/callout.component.html @@ -3,10 +3,14 @@ [ngClass]="calloutClass" [attr.aria-labelledby]="titleId" > -
- - {{ title }} -
+ @if (title) { +
+ @if (icon) { + + } + {{ title }} +
+ }
diff --git a/libs/components/src/card/card.component.ts b/libs/components/src/card/card.component.ts index 37756088e0d..fdb02f280da 100644 --- a/libs/components/src/card/card.component.ts +++ b/libs/components/src/card/card.component.ts @@ -1,10 +1,8 @@ -import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component } from "@angular/core"; @Component({ selector: "bit-card", standalone: true, - imports: [CommonModule], template: ``, changeDetection: ChangeDetectionStrategy.OnPush, host: { diff --git a/libs/components/src/chip-select/chip-select.component.html b/libs/components/src/chip-select/chip-select.component.html index 81480f107f1..e88200b6e4f 100644 --- a/libs/components/src/chip-select/chip-select.component.html +++ b/libs/components/src/chip-select/chip-select.component.html @@ -30,78 +30,80 @@ {{ label }} - + @if (!selectedOption) { + + } - + @if (selectedOption) { + + }
-
- - - - - - - -
+ @if (getParent(renderedOptions); as parent) { + + + } + @for (option of renderedOptions.children; track option) { + + } +
+ } diff --git a/libs/components/src/chip-select/chip-select.component.ts b/libs/components/src/chip-select/chip-select.component.ts index a653d79f83f..39543db5ed5 100644 --- a/libs/components/src/chip-select/chip-select.component.ts +++ b/libs/components/src/chip-select/chip-select.component.ts @@ -46,6 +46,7 @@ export type ChipSelectOption = Option & { multi: true, }, ], + preserveWhitespaces: false, }) export class ChipSelectComponent implements ControlValueAccessor, AfterViewInit { @ViewChild(MenuComponent) menu: MenuComponent; diff --git a/libs/components/src/color-password/color-password.component.ts b/libs/components/src/color-password/color-password.component.ts index cbf746e9d73..e48758ca59a 100644 --- a/libs/components/src/color-password/color-password.component.ts +++ b/libs/components/src/color-password/color-password.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgFor, NgIf } from "@angular/common"; + import { Component, HostBinding, Input } from "@angular/core"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -14,18 +14,16 @@ enum CharacterType { @Component({ selector: "bit-color-password", - template: ` - {{ character }} - {{ - i + 1 - }} - `, + template: `@for (character of passwordArray; track character; let i = $index) { + + {{ character }} + @if (showCount) { + {{ i + 1 }} + } + + }`, preserveWhitespaces: false, standalone: true, - imports: [NgFor, NgIf], }) export class ColorPasswordComponent { @Input() password: string = null; diff --git a/libs/components/src/color-password/index.ts b/libs/components/src/color-password/index.ts index 86718f037f7..24870ca75d9 100644 --- a/libs/components/src/color-password/index.ts +++ b/libs/components/src/color-password/index.ts @@ -1 +1,2 @@ export * from "./color-password.module"; +export * from "./color-password.component"; diff --git a/libs/components/src/container/container.component.ts b/libs/components/src/container/container.component.ts index fbd9e40a7ca..1bcdb8f459b 100644 --- a/libs/components/src/container/container.component.ts +++ b/libs/components/src/container/container.component.ts @@ -1,4 +1,3 @@ -import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; /** @@ -7,7 +6,6 @@ import { Component } from "@angular/core"; @Component({ selector: "bit-container", templateUrl: "container.component.html", - imports: [CommonModule], standalone: true, }) export class ContainerComponent {} diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 06a7772edbe..01f05985127 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -13,9 +13,11 @@ class="tw-text-main tw-mb-0 tw-truncate" > {{ title }} - - {{ subtitle }} - + @if (subtitle) { + + {{ subtitle }} + + }

+ @if (showCancelButton) { + + }
diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts index 60b2e1c3a3f..00026209183 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts @@ -1,7 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; -import { NgIf } from "@angular/common"; import { Component, Inject } from "@angular/core"; import { FormGroup, ReactiveFormsModule } from "@angular/forms"; @@ -39,7 +38,6 @@ const DEFAULT_COLOR: Record = { IconDirective, ButtonComponent, BitFormButtonDirective, - NgIf, ], }) export class SimpleConfigurableDialogComponent { diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts index b4bf199358b..87d6eb9fbfc 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts @@ -12,23 +12,24 @@ import { DialogModule } from "../../dialog.module"; @Component({ template: ` -
-

{{ group.title }}

-
- + @for (group of dialogs; track group) { +
+

{{ group.title }}

+
+ @for (dialog of group.dialogs; track dialog) { + + } +
-
+ } - - {{ dialogCloseResult }} - + @if (showCallout) { + + {{ dialogCloseResult }} + + } `, }) class StoryDialogComponent { diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.component.html b/libs/components/src/dialog/simple-dialog/simple-dialog.component.html index 0b56c6287dc..1f154a8d543 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.component.html +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.component.html @@ -3,12 +3,11 @@ @fadeIn >
- + @if (hasIcon) { - - + } @else { - + }

- ({{ "required" | i18n }}) + @if (required) { + ({{ "required" | i18n }}) + } - + @if (!hasError) { + + } -
- {{ displayError }} -
+@if (hasError) { +
+ {{ displayError }} +
+} diff --git a/libs/components/src/form-control/form-control.component.ts b/libs/components/src/form-control/form-control.component.ts index d22d49ac03a..690c00a9dc0 100644 --- a/libs/components/src/form-control/form-control.component.ts +++ b/libs/components/src/form-control/form-control.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; -import { NgClass, NgIf } from "@angular/common"; +import { NgClass } from "@angular/common"; import { Component, ContentChild, HostBinding, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -15,7 +15,7 @@ import { BitFormControlAbstraction } from "./form-control.abstraction"; selector: "bit-form-control", templateUrl: "form-control.component.html", standalone: true, - imports: [NgClass, TypographyDirective, NgIf, I18nPipe], + imports: [NgClass, TypographyDirective, I18nPipe], }) export class FormControlComponent { @Input() label: string; diff --git a/libs/components/src/form-control/label.component.html b/libs/components/src/form-control/label.component.html index 64ba1ce9501..2a0a57e35d8 100644 --- a/libs/components/src/form-control/label.component.html +++ b/libs/components/src/form-control/label.component.html @@ -5,10 +5,10 @@ - + @if (isInsideFormControl) { - + } - +@if (!isInsideFormControl) { - +} diff --git a/libs/components/src/form-field/error-summary.component.ts b/libs/components/src/form-field/error-summary.component.ts index f9325d8f82a..1709c3078fa 100644 --- a/libs/components/src/form-field/error-summary.component.ts +++ b/libs/components/src/form-field/error-summary.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf } from "@angular/common"; + import { Component, Input } from "@angular/core"; import { AbstractControl, UntypedFormGroup } from "@angular/forms"; @@ -8,15 +8,15 @@ import { I18nPipe } from "@bitwarden/ui-common"; @Component({ selector: "bit-error-summary", - template: ` + template: ` @if (errorCount > 0) { {{ "fieldsNeedAttention" | i18n: errorString }} - `, + }`, host: { class: "tw-block tw-text-danger tw-mt-2", "aria-live": "assertive", }, standalone: true, - imports: [NgIf, I18nPipe], + imports: [I18nPipe], }) export class BitErrorSummary { @Input() diff --git a/libs/components/src/form-field/form-field.component.html b/libs/components/src/form-field/form-field.component.html index d2771f42465..bd099859608 100644 --- a/libs/components/src/form-field/form-field.component.html +++ b/libs/components/src/form-field/form-field.component.html @@ -15,63 +15,65 @@ -
-
-
-
-
-
-
-
- -
-
- -
-
- -
-
-
- - +} @else {
- +} - - - - +@switch (input.hasError) { + @case (false) { + + } + @case (true) { + + } +} diff --git a/libs/components/src/form-field/index.ts b/libs/components/src/form-field/index.ts index 613ebaf9a9d..0c45f215ec9 100644 --- a/libs/components/src/form-field/index.ts +++ b/libs/components/src/form-field/index.ts @@ -1,4 +1,5 @@ export * from "./form-field.module"; export * from "./form-field.component"; export * from "./form-field-control"; +export * from "./password-input-toggle.directive"; export * as BitValidators from "./bit-validators"; diff --git a/libs/components/src/input/index.ts b/libs/components/src/input/index.ts index 9713d2b9192..6bd64495910 100644 --- a/libs/components/src/input/index.ts +++ b/libs/components/src/input/index.ts @@ -1,2 +1,3 @@ export * from "./input.module"; export * from "./autofocus.directive"; +export * from "./input.directive"; diff --git a/libs/components/src/item/item-content.component.html b/libs/components/src/item/item-content.component.html index 6f900c5c6e1..4010970dc9e 100644 --- a/libs/components/src/item/item-content.component.html +++ b/libs/components/src/item/item-content.component.html @@ -6,14 +6,26 @@ bitTypography="body2" class="tw-text-main tw-truncate tw-inline-flex tw-items-center tw-gap-1.5 tw-w-full" > -
+
-
+
diff --git a/libs/components/src/item/item-content.component.ts b/libs/components/src/item/item-content.component.ts index f6cc3f133ad..2a6e06291fd 100644 --- a/libs/components/src/item/item-content.component.ts +++ b/libs/components/src/item/item-content.component.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CommonModule } from "@angular/common"; + +import { NgClass } from "@angular/common"; import { AfterContentChecked, ChangeDetectionStrategy, Component, ElementRef, + Input, signal, ViewChild, } from "@angular/core"; @@ -15,7 +17,7 @@ import { TypographyModule } from "../typography"; @Component({ selector: "bit-item-content, [bit-item-content]", standalone: true, - imports: [CommonModule, TypographyModule], + imports: [TypographyModule, NgClass], templateUrl: `item-content.component.html`, host: { class: @@ -32,6 +34,13 @@ export class ItemContentComponent implements AfterContentChecked { protected endSlotHasChildren = signal(false); + /** + * Determines whether text will truncate or wrap. + * + * Default behavior is truncation. + */ + @Input() truncate = true; + ngAfterContentChecked(): void { this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0); } diff --git a/libs/components/src/item/item.component.ts b/libs/components/src/item/item.component.ts index 97a80484373..1ef4a4af1fa 100644 --- a/libs/components/src/item/item.component.ts +++ b/libs/components/src/item/item.component.ts @@ -1,4 +1,3 @@ -import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component, @@ -14,7 +13,7 @@ import { ItemActionComponent } from "./item-action.component"; @Component({ selector: "bit-item", standalone: true, - imports: [CommonModule, ItemActionComponent], + imports: [ItemActionComponent], changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: "item.component.html", providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }], diff --git a/libs/components/src/item/item.mdx b/libs/components/src/item/item.mdx index ca697ebb436..ab39e738b26 100644 --- a/libs/components/src/item/item.mdx +++ b/libs/components/src/item/item.mdx @@ -111,6 +111,30 @@ Actions are commonly icon buttons or badge buttons. ``` +## Text Overflow Behavior + +The default behavior for long text is to truncate it. However, you have the option of changing it to +wrap instead if that is what the design calls for. + +This can be changed by passing `[truncate]="false"` to the `bit-item-content`. + +```html + + + Long text goes here! + This could also be very long text + + +``` + +### Truncation (Default) + + + +### Wrap + + + ## Item Groups Groups of items can be associated by wrapping them in the ``. diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts index 3a64a334d0a..fd2d59c7ac2 100644 --- a/libs/components/src/item/item.stories.ts +++ b/libs/components/src/item/item.stories.ts @@ -135,7 +135,7 @@ export const ContentTypes: Story = { }), }; -export const TextOverflow: Story = { +export const TextOverflowTruncate: Story = { render: (args) => ({ props: args, template: /*html*/ ` @@ -158,6 +158,29 @@ export const TextOverflow: Story = { }), }; +export const TextOverflowWrap: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! + Worlddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd! + + + + + + + + + + + `, + }), +}; + const multipleActionListTemplate = /*html*/ ` diff --git a/libs/components/src/layout/layout.component.html b/libs/components/src/layout/layout.component.html index 7c1c5b2501d..33b8de81572 100644 --- a/libs/components/src/layout/layout.component.html +++ b/libs/components/src/layout/layout.component.html @@ -23,19 +23,21 @@ -
+ }; + as data + ) {
-
+ class="tw-pointer-events-none tw-fixed tw-inset-0 tw-z-10 tw-bg-black tw-bg-opacity-0 motion-safe:tw-transition-colors md:tw-hidden" + [ngClass]="[data.open ? 'tw-bg-opacity-30 md:tw-bg-opacity-0' : 'tw-bg-opacity-0']" + > + @if (data.open) { +
+ } +

+ }
diff --git a/libs/components/src/multi-select/multi-select.component.html b/libs/components/src/multi-select/multi-select.component.html index 0c45e2d333f..e157871e17a 100644 --- a/libs/components/src/multi-select/multi-select.component.html +++ b/libs/components/src/multi-select/multi-select.component.html @@ -31,7 +31,9 @@ [disabled]="disabled" (click)="clear(item)" > - + @if (item.icon != null) { + + } {{ item.labelName }} @@ -41,10 +43,14 @@
- + @if (isSelected(item)) { + + }
- + @if (item.icon != null) { + + }
{{ item.listName }} diff --git a/libs/components/src/multi-select/multi-select.component.ts b/libs/components/src/multi-select/multi-select.component.ts index 71b00404cfb..cd92eb1d7ae 100644 --- a/libs/components/src/multi-select/multi-select.component.ts +++ b/libs/components/src/multi-select/multi-select.component.ts @@ -2,7 +2,6 @@ // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { hasModifierKey } from "@angular/cdk/keycodes"; -import { NgIf } from "@angular/common"; import { Component, Input, @@ -39,7 +38,7 @@ let nextId = 0; templateUrl: "./multi-select.component.html", providers: [{ provide: BitFormFieldControl, useExisting: MultiSelectComponent }], standalone: true, - imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, NgIf, I18nPipe], + imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, I18nPipe], }) /** * This component has been implemented to only support Multi-select list events diff --git a/libs/components/src/navigation/nav-divider.component.html b/libs/components/src/navigation/nav-divider.component.html index 224f6ae0657..64e43aeab4e 100644 --- a/libs/components/src/navigation/nav-divider.component.html +++ b/libs/components/src/navigation/nav-divider.component.html @@ -1 +1,3 @@ -
+@if (sideNavService.open$ | async) { +
+} diff --git a/libs/components/src/navigation/nav-group.component.html b/libs/components/src/navigation/nav-group.component.html index 9f6d9ac034d..9752fe56eb1 100644 --- a/libs/components/src/navigation/nav-group.component.html +++ b/libs/components/src/navigation/nav-group.component.html @@ -1,5 +1,5 @@ - +@if (!hideIfEmpty || nestedNavComponents.length > 0) { - - - - - - - + @if (variant === "tree") { + + } + + + @if (variant !== "tree") { + + } - - -
- -
-
-
+ @if (sideNavService.open$ | async) { + @if (open) { +
+ +
+ } + } +} diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts index 37244f37c8d..62bdee26740 100644 --- a/libs/components/src/navigation/nav-group.component.ts +++ b/libs/components/src/navigation/nav-group.component.ts @@ -29,6 +29,7 @@ import { SideNavService } from "./side-nav.service"; ], standalone: true, imports: [CommonModule, NavItemComponent, IconButtonModule, I18nPipe], + preserveWhitespaces: false, }) export class NavGroupComponent extends NavBaseComponent implements AfterContentInit { @ContentChildren(NavBaseComponent, { diff --git a/libs/components/src/navigation/nav-logo.component.html b/libs/components/src/navigation/nav-logo.component.html index 427e926f2d7..a6169315333 100644 --- a/libs/components/src/navigation/nav-logo.component.html +++ b/libs/components/src/navigation/nav-logo.component.html @@ -1,20 +1,23 @@ - - +@if (sideNavService.open) { +
+ + + +
+} +@if (!sideNavService.open) { + +} diff --git a/libs/components/src/navigation/nav-logo.component.ts b/libs/components/src/navigation/nav-logo.component.ts index 8a84970500c..de9d801e553 100644 --- a/libs/components/src/navigation/nav-logo.component.ts +++ b/libs/components/src/navigation/nav-logo.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf } from "@angular/common"; + import { Component, Input } from "@angular/core"; import { RouterLinkActive, RouterLink } from "@angular/router"; @@ -14,7 +14,7 @@ import { SideNavService } from "./side-nav.service"; selector: "bit-nav-logo", templateUrl: "./nav-logo.component.html", standalone: true, - imports: [NgIf, RouterLinkActive, RouterLink, BitIconComponent, NavItemComponent], + imports: [RouterLinkActive, RouterLink, BitIconComponent, NavItemComponent], }) export class NavLogoComponent { /** Icon that is displayed when the side nav is closed */ diff --git a/libs/components/src/navigation/side-nav.component.html b/libs/components/src/navigation/side-nav.component.html index 05c99c7d64e..3b77c981be4 100644 --- a/libs/components/src/navigation/side-nav.component.html +++ b/libs/components/src/navigation/side-nav.component.html @@ -1,42 +1,46 @@ - + +} diff --git a/libs/components/src/progress/progress.component.html b/libs/components/src/progress/progress.component.html index 2637f23eee8..30b68d9d645 100644 --- a/libs/components/src/progress/progress.component.html +++ b/libs/components/src/progress/progress.component.html @@ -7,13 +7,12 @@ attr.aria-valuenow="{{ barWidth }}" [ngStyle]="{ width: barWidth + '%' }" > -
- -
 
-
{{ textContent }}
-
+ @if (displayText) { +
+ +
 
+
{{ textContent }}
+
+ }
diff --git a/libs/components/src/radio-button/radio-group.component.html b/libs/components/src/radio-button/radio-group.component.html index 128a723d461..b71abd9249c 100644 --- a/libs/components/src/radio-button/radio-group.component.html +++ b/libs/components/src/radio-button/radio-group.component.html @@ -1,16 +1,18 @@ - +@if (label) {
- ({{ "required" | i18n }}) + @if (required) { + ({{ "required" | i18n }}) + }
-
+} - +@if (!label) { - +}
diff --git a/libs/components/src/radio-button/radio-group.component.ts b/libs/components/src/radio-button/radio-group.component.ts index 4ab626f7964..895f769af50 100644 --- a/libs/components/src/radio-button/radio-group.component.ts +++ b/libs/components/src/radio-button/radio-group.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf, NgTemplateOutlet } from "@angular/common"; +import { NgTemplateOutlet } from "@angular/common"; import { Component, ContentChild, HostBinding, Input, Optional, Self } from "@angular/core"; import { ControlValueAccessor, NgControl, Validators } from "@angular/forms"; @@ -14,7 +14,7 @@ let nextId = 0; selector: "bit-radio-group", templateUrl: "radio-group.component.html", standalone: true, - imports: [NgIf, NgTemplateOutlet, I18nPipe], + imports: [NgTemplateOutlet, I18nPipe], }) export class RadioGroupComponent implements ControlValueAccessor { selected: unknown; diff --git a/libs/components/src/select/select.component.html b/libs/components/src/select/select.component.html index 848692526a1..dcca7ae195e 100644 --- a/libs/components/src/select/select.component.html +++ b/libs/components/src/select/select.component.html @@ -13,7 +13,9 @@
- + @if (item.icon != null) { + + }
{{ item.label }} diff --git a/libs/components/src/select/select.component.ts b/libs/components/src/select/select.component.ts index 8f75c5be42b..a89eb87f54b 100644 --- a/libs/components/src/select/select.component.ts +++ b/libs/components/src/select/select.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgIf } from "@angular/common"; + import { Component, ContentChildren, @@ -36,7 +36,7 @@ let nextId = 0; templateUrl: "select.component.html", providers: [{ provide: BitFormFieldControl, useExisting: SelectComponent }], standalone: true, - imports: [NgSelectModule, ReactiveFormsModule, FormsModule, NgIf], + imports: [NgSelectModule, ReactiveFormsModule, FormsModule], }) export class SelectComponent implements BitFormFieldControl, ControlValueAccessor { @ViewChild(NgSelectComponent) select: NgSelectComponent; diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts index c63b36ea89c..5fc01d37d53 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts @@ -49,11 +49,9 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; Your favorite color - + @for (color of colors; track color) { + + } diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts index 568c78566f6..9c609300ed1 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -36,9 +36,11 @@ class KitchenSinkDialog {

- - {{ item.name }} - + @for (item of navItems; track item) { + + {{ item.name }} + + }

diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts index 6f0054912cf..c71140d8166 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts @@ -16,11 +16,15 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; Mid-sized 1
-
    -
  • - {{ company.name }} -
  • -
+ @for (company of companyList; track company) { +
    + @if (company.size === selectedToggle || selectedToggle === "all") { +
  • + {{ company.name }} +
  • + } +
+ } `, }) export class KitchenSinkToggleList { diff --git a/libs/components/src/tabs/shared/tab-list-item.directive.ts b/libs/components/src/tabs/shared/tab-list-item.directive.ts index 87435133a23..2a71a385a83 100644 --- a/libs/components/src/tabs/shared/tab-list-item.directive.ts +++ b/libs/components/src/tabs/shared/tab-list-item.directive.ts @@ -44,7 +44,7 @@ export class TabListItemDirective implements FocusableOption { */ get textColorClassList(): string[] { if (this.disabled) { - return ["!tw-text-muted", "hover:!tw-text-muted"]; + return ["!tw-text-secondary-300", "hover:!tw-text-secondary-300"]; } if (this.active) { return ["!tw-text-primary-600", "hover:!tw-text-primary-700"]; @@ -60,7 +60,7 @@ export class TabListItemDirective implements FocusableOption { "tw-px-4", "tw-font-semibold", "tw-transition", - "tw-rounded-t", + "tw-rounded-t-lg", "tw-border-0", "tw-border-x", "tw-border-t-4", @@ -71,12 +71,12 @@ export class TabListItemDirective implements FocusableOption { "focus-visible:tw-z-10", "focus-visible:tw-outline-none", "focus-visible:tw-ring-2", - "focus-visible:tw-ring-primary-700", + "focus-visible:tw-ring-primary-600", ]; } get disabledClassList(): string[] { - return ["!tw-bg-secondary-100", "!tw-no-underline", "tw-cursor-not-allowed"]; + return ["!tw-no-underline", "tw-cursor-not-allowed"]; } get activeClassList(): string[] { @@ -87,6 +87,7 @@ export class TabListItemDirective implements FocusableOption { "tw-border-b", "tw-border-b-background", "!tw-bg-background", + "hover:tw-no-underline", "hover:tw-border-t-primary-700", "focus-visible:tw-border-t-primary-700", "focus-visible:!tw-text-primary-700", diff --git a/libs/components/src/tabs/tab-group/tab-group.component.html b/libs/components/src/tabs/tab-group/tab-group.component.html index 071f5c2259f..52fa193de96 100644 --- a/libs/components/src/tabs/tab-group/tab-group.component.html +++ b/libs/components/src/tabs/tab-group/tab-group.component.html @@ -5,40 +5,41 @@ [attr.aria-label]="label" (keydown)="keyManager.onKeydown($event)" > - + + }
- - + @for (tab of tabs; track tab; let i = $index) { + + + }
diff --git a/libs/components/src/tabs/tab-group/tab-group.component.ts b/libs/components/src/tabs/tab-group/tab-group.component.ts index 54d00343b38..b525b9b6723 100644 --- a/libs/components/src/tabs/tab-group/tab-group.component.ts +++ b/libs/components/src/tabs/tab-group/tab-group.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { FocusKeyManager } from "@angular/cdk/a11y"; import { coerceNumberProperty } from "@angular/cdk/coercion"; -import { CommonModule } from "@angular/common"; +import { NgTemplateOutlet } from "@angular/common"; import { AfterContentChecked, AfterContentInit, @@ -33,7 +33,7 @@ let nextId = 0; templateUrl: "./tab-group.component.html", standalone: true, imports: [ - CommonModule, + NgTemplateOutlet, TabHeaderComponent, TabListContainerDirective, TabListItemDirective, diff --git a/libs/components/src/toast/toast.component.html b/libs/components/src/toast/toast.component.html index 84154cba611..d78cc7783aa 100644 --- a/libs/components/src/toast/toast.component.html +++ b/libs/components/src/toast/toast.component.html @@ -7,15 +7,14 @@
{{ variant | i18n }} -

{{ title }}

-

- {{ m }} -

+ @if (title) { +

{{ title }}

+ } + @for (m of messageArray; track m) { +

+ {{ m }} +

+ }