diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 22c14f6b433..255ddc08c80 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -162,6 +162,7 @@ apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-desktop- libs/components @bitwarden/team-ui-foundation libs/assets @bitwarden/team-ui-foundation libs/ui @bitwarden/team-ui-foundation +libs/angular/src/scss @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/browser/src/popup/components/extension-anon-layout-wrapper @bitwarden/team-ui-foundation diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 8e31ab7a384..126d66b34e3 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -147,6 +147,7 @@ "@nx/eslint", "@nx/jest", "@nx/js", + "@nx/webpack", "@types/chrome", "@types/firefox-webext-browser", "@types/glob", diff --git a/.github/workflows/alert-ddg-files-modified.yml b/.github/workflows/alert-ddg-files-modified.yml index c341de045eb..d799cc2e248 100644 --- a/.github/workflows/alert-ddg-files-modified.yml +++ b/.github/workflows/alert-ddg-files-modified.yml @@ -30,7 +30,7 @@ jobs: - 'apps/desktop/src/services/encrypted-message-handler.service.ts' - name: Remove past BIT status comments - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | // Note: should match the first line of `message` in the communication steps @@ -67,7 +67,7 @@ jobs: - name: Comment on PR if monitored files changed if: steps.changed-files.outputs.monitored == 'true' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const changedFiles = `${{ steps.changed-files.outputs.monitored_files }}`.split(' ').filter(file => file.trim() !== ''); diff --git a/.github/workflows/auto-reply-discussions.yml b/.github/workflows/auto-reply-discussions.yml index 83970ab3619..a6d7e9c6dcf 100644 --- a/.github/workflows/auto-reply-discussions.yml +++ b/.github/workflows/auto-reply-discussions.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Get discussion label and template name id: discussion-label - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const discussion = context.payload.discussion; @@ -29,7 +29,7 @@ jobs: - name: Get selected topic id: get_selected_topic - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: result-encoding: string script: | @@ -45,7 +45,7 @@ jobs: } - name: Reply or close Discussion - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: TEMPLATE_NAME: ${{ steps.discussion-label.outputs.template_name }} TOPIC: ${{ steps.get_selected_topic.outputs.result }} diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 18887ce8fbc..d623f08ebac 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -409,7 +409,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Trigger web vault deploy using GitHub Run ID - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} script: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7eab45e5b1b..738aef899f5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -49,6 +49,7 @@ jobs: ! -path "*/Cargo.toml" \ ! -path "*/Cargo.lock" \ ! -path "./apps/desktop/macos/*" \ + ! -path "*/CLAUDE.md" \ > tmp.txt diff <(sort .github/whitelist-capital-letters.txt) <(sort tmp.txt) diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index a6f0f1be066..6446e625156 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -179,7 +179,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Trigger self-host build - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} script: | diff --git a/.github/workflows/review-code.yml b/.github/workflows/review-code.yml new file mode 100644 index 00000000000..b49f5cec8f0 --- /dev/null +++ b/.github/workflows/review-code.yml @@ -0,0 +1,109 @@ +name: Review code + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: {} + +jobs: + review: + name: Review + runs-on: ubuntu-24.04 + permissions: + contents: read + id-token: write + pull-requests: write + + steps: + - name: Check out repo + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Check for Vault team changes + id: check_changes + run: | + # Ensure we have the base branch + git fetch origin ${{ github.base_ref }} + + echo "Comparing changes between origin/${{ github.base_ref }} and HEAD" + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) + + if [ -z "$CHANGED_FILES" ]; then + echo "Zero files changed" + echo "vault_team_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Handle variations in spacing and multiple teams + VAULT_PATTERNS=$(grep -E "@bitwarden/team-vault-dev(\s|$)" .github/CODEOWNERS 2>/dev/null | awk '{print $1}') + + if [ -z "$VAULT_PATTERNS" ]; then + echo "⚠️ No patterns found for @bitwarden/team-vault-dev in CODEOWNERS" + echo "vault_team_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + vault_team_changes=false + for pattern in $VAULT_PATTERNS; do + echo "Checking pattern: $pattern" + + # Handle **/directory patterns + if [[ "$pattern" == "**/"* ]]; then + # Remove the **/ prefix + dir_pattern="${pattern#\*\*/}" + # Check if any file contains this directory in its path + if echo "$CHANGED_FILES" | grep -qE "(^|/)${dir_pattern}(/|$)"; then + vault_team_changes=true + echo "✅ Found files matching pattern: $pattern" + echo "$CHANGED_FILES" | grep -E "(^|/)${dir_pattern}(/|$)" | sed 's/^/ - /' + break + fi + else + # Handle other patterns (shouldn't happen based on your CODEOWNERS) + if echo "$CHANGED_FILES" | grep -q "$pattern"; then + vault_team_changes=true + echo "✅ Found files matching pattern: $pattern" + echo "$CHANGED_FILES" | grep "$pattern" | sed 's/^/ - /' + break + fi + fi + done + + echo "vault_team_changes=$vault_team_changes" >> $GITHUB_OUTPUT + + if [ "$vault_team_changes" = "true" ]; then + echo "" + echo "✅ Vault team changes detected - proceeding with review" + else + echo "" + echo "❌ No Vault team changes detected - skipping review" + fi + + - name: Review with Claude Code + if: steps.check_changes.outputs.vault_team_changes == 'true' + uses: anthropics/claude-code-action@a5528eec7426a4f0c9c1ac96018daa53ebd05bc4 # v1.0.7 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + track_progress: true + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + TITLE: ${{ github.event.pull_request.title }} + BODY: ${{ github.event.pull_request.body }} + AUTHOR: ${{ github.event.pull_request.user.login }} + + Please review this pull request with a focus on: + - Code quality and best practices + - Potential bugs or issues + - Security implications + - Performance considerations + + Note: The PR branch is already checked out in the current working directory. + + Provide detailed feedback using inline comments for specific issues. + + claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64c4e0dff13..2770c1257ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -125,8 +125,8 @@ jobs: - name: Test Windows if: ${{ matrix.os=='windows-2022'}} - working-directory: ./apps/desktop/desktop_native/core - run: cargo test -- --test-threads=1 + working-directory: ./apps/desktop/desktop_native + run: cargo test --workspace --exclude=desktop_napi -- --test-threads=1 rust-coverage: name: Rust Coverage diff --git a/.gitignore b/.gitignore index 0f609335b63..61a20195592 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ Thumbs.db .settings/ *.sublime-workspace .claude +.serena # Visual Studio Code .vscode/* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..0870553f8d3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# Bitwarden Clients - Claude Code Configuration + +## Critical Rules + +- **NEVER** use code regions: If complexity suggests regions, refactor for better readability + +- **CRITICAL**: new encryption logic should not be added to this repo. + +- **NEVER** send unencrypted vault data to API services + +- **NEVER** commit secrets, credentials, or sensitive information. Follow the guidelines in `SECURITY.md`. + +- **NEVER** log decrypted data, encryption keys, or PII + - No vault data in error messages or console logs + +- **ALWAYS** Respect configuration files at the root and within each app/library (e.g., `eslint.config.mjs`, `jest.config.js`, `tsconfig.json`). + +## Mono-Repo Architecture + +This repository is organized as a **monorepo** containing multiple applications and libraries. The +main directories are: + +- `apps/` – Contains all application projects (e.g., browser, cli, desktop, web). Each app is + self-contained with its own configuration, source code, and tests. +- `libs/` – Contains shared libraries and modules used across multiple apps. Libraries are organized + by team name, domain, functionality (e.g., common, ui, platform, key-management). + +**Strict boundaries** must be maintained between apps and libraries. Do not introduce +cross-dependencies that violate the intended modular structure. Always consult and respect the +dependency rules defined in `eslint.config.mjs`, `nx.json`, and other configuration files. + +## Angular Architecture Patterns + +**Observable Data Services (ADR-0003):** + +- Services expose RxJS Observable streams for state management +- Components subscribe using `async` pipe (NOT explicit subscriptions in most cases) + Pattern: + +```typescript +// Service +private _folders = new BehaviorSubject([]); +readonly folders$ = this._folders.asObservable(); + +// Component +folders$ = this.folderService.folders$; +// Template:
+``` + +For explicit subscriptions, MUST use `takeUntilDestroyed()`: + +```typescript +constructor() { + this.observable$.pipe(takeUntilDestroyed()).subscribe(...); +} +``` + +**Angular Signals (ADR-0027):** + +Encourage the use of Signals **only** in Angular components and presentational services. + +Use **RxJS** for: + +- Services used across Angular and non-Angular clients +- Complex reactive workflows +- Interop with existing Observable-based code + +**NO TypeScript Enums (ADR-0025):** + +- Use const objects with type aliases instead +- Legacy enums exist but don't add new ones + +Pattern: + +```typescript +// ✅ DO +export const CipherType = Object.freeze({ + Login: 1, + SecureNote: 2, +} as const); +export type CipherType = (typeof CipherType)[keyof typeof CipherType]; + +// ❌ DON'T +enum CipherType { + Login = 1, + SecureNote = 2, +} +``` + +Example: `/libs/common/src/vault/enums/cipher-type.ts` diff --git a/apps/browser/CLAUDE.md b/apps/browser/CLAUDE.md new file mode 100644 index 00000000000..a718f5bfd7c --- /dev/null +++ b/apps/browser/CLAUDE.md @@ -0,0 +1,22 @@ +# Browser Extension - Critical Rules + +- **NEVER** use `chrome.*` or `browser.*` APIs directly in business logic + - Always use `BrowserApi` abstraction: `/apps/browser/src/platform/browser/browser-api.ts` + - Required for cross-browser compatibility (Chrome/Firefox/Safari/Opera) + +- **ALWAYS** use `BrowserApi.addListener()` for event listeners in popup context + - Safari requires manual cleanup to prevent memory leaks + - DON'T use native `chrome.*.addListener()` or `browser.*.addListener()` directly + +- **CRITICAL**: Safari has tab query bugs + - Use `BrowserApi.tabsQueryFirstCurrentWindowForSafari()` when querying current window tabs + - Safari can return tabs from multiple windows incorrectly + +## Manifest V3 + +- Extension uses Web Extension API Manifest V3 +- **Service workers replace background pages** + - Background context runs as service worker (can be terminated anytime) + - DON'T assume background page persists indefinitely + - Use message passing for communication between contexts + - `chrome.extension.getBackgroundPage()` returns `null` in MV3 diff --git a/apps/browser/package.json b/apps/browser/package.json index 24a53f43f66..402a00fee31 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.9.0", + "version": "2025.10.0", "scripts": { "build": "npm run build:chrome", "build:bit": "npm run build:bit:chrome", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 397ea877cb5..e8a37262993 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "كلمة المرور الرئيسية غير صالحة" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "نفذ وقت الخزانة" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "خطأ" }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 639e6c87d36..c877b87f90f 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Axtarışı sıfırla" }, - "archive": { - "message": "Arxivlə" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Arxivdən çıxart" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Yararsız ana parol" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Seyf vaxtının bitməsi" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Xəta" }, diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 44c82ef85b4..fb645e0b815 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Памылковы асноўны пароль" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Час чакання сховішча" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Памылка" }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index a440690cee1..d27e426d4aa 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Нулиране на търсенето" }, - "archive": { - "message": "Архивиране" + "archiveNoun": { + "message": "Архив", + "description": "Noun" + }, + "archiveVerb": { + "message": "Архивиране", + "description": "Verb" }, "unarchive": { "message": "Изваждане от архива" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Грешна главна парола" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Грешна главна парола. Проверете дали е-пощата е правилна и дали акаунтът Ви е създаден в $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Време за достъп" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Ще бъдат изнесени само записите от трезора свързан с $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Грешка" }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index e7c4c36bce0..66d3fbe598a 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "অবৈধ মূল পাসওয়ার্ড" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "ভল্টের সময়সীমা" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index d9003a749a6..39df44ca95e 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 2002dfc467f..7f897e9cb4c 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Restableix la cerca" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Contrasenya mestra no vàlida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Temps d'espera de la caixa forta" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 0638257d687..8820af69f98 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Resetovat hledání" }, - "archive": { - "message": "Archivovat" + "archiveNoun": { + "message": "Archiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archivovat", + "description": "Verb" }, "unarchive": { "message": "Odebrat z archivu" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Chybné hlavní heslo" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Neplatné hlavní heslo. Potvrďte správnost e-mailu a zda byl Váš účet vytvořen na $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Časový limit trezoru" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Bude exportován jen trezor organizace přidružený k $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Bude exportován jen trezor organizace přidružený k $ORGANIZATION$. Položky mých sbírek nebudou zahrnuty.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Chyba" }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 8756a138e81..ded0ad406df 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Prif gyfrinair annilys" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Cloi'r gell" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Gwall" }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index a78ff26fb0f..04d287947ee 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Ugyldig hovedadgangskode" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Boks timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Fejl" }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index f04ca5b11be..ce26078d427 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -550,20 +550,25 @@ "resetSearch": { "message": "Suche zurücksetzen" }, - "archive": { - "message": "Archivieren" + "archiveNoun": { + "message": "Archiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archivieren", + "description": "Verb" }, "unarchive": { - "message": "Archivierung aufheben" + "message": "Nicht mehr archivieren" }, "itemsInArchive": { "message": "Einträge im Archiv" }, "noItemsInArchive": { - "message": "Kein Eintrag im Archiv" + "message": "Keine Einträge im Archiv" }, "noItemsInArchiveDesc": { - "message": "Archivierte Einträge erscheinen hier und werden von allgemeinen Suchergebnissen und Autofill Vorschlägen ausgeschlossen." + "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen sowie Vorschlägen zum automatischen Ausfüllen ausgeschlossen." }, "itemSentToArchive": { "message": "Eintrag an das Archiv gesendet" @@ -575,7 +580,7 @@ "message": "Eintrag archivieren" }, "archiveItemConfirmDesc": { - "message": "Archivierte Einträge sind von allgemeinen Suchergebnissen und Autofill Vorschlägen ausgeschlossen. Sind Sie sicher, dass Sie diesen Eintrag archivieren möchten?" + "message": "Archivierte Einträge werden von allgemeinen Suchergebnissen sowie Vorschlägen zum automatischen Ausfüllen ausgeschlossen. Bist du sicher, dass du diesen Eintrag archivieren möchtest?" }, "edit": { "message": "Bearbeiten" @@ -584,7 +589,7 @@ "message": "Anzeigen" }, "viewLogin": { - "message": "Login ansehen" + "message": "Zugangsdaten anzeigen" }, "noItemsInList": { "message": "Keine Einträge zum Anzeigen vorhanden." @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Ungültiges Master-Passwort" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ungültiges Master-Passwort. Überprüfe, ob deine E-Mail-Adresse korrekt ist und dein Konto auf $HOST$ erstellt wurde.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Tresor-Timeout" }, @@ -920,7 +934,7 @@ "message": "Folge den Schritten unten, um die Anmeldung abzuschließen." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Folge den Schritten unten, um die Anmeldung mit deinem Sicherheitsschlüssel abzuschließen." + "message": "Folge den untenstehenden Schritten, um die Anmeldung mit deinem Sicherheitsschlüssel abzuschließen." }, "restartRegistration": { "message": "Registrierung neu starten" @@ -1782,7 +1796,7 @@ "message": "Dieses Pop-up Fenster wird geschlossen, wenn du außerhalb des Fensters klickst um in deinen E-Mails nach dem Verifizierungscode zu suchen. Möchtest du, dass dieses Pop-up in einem separaten Fenster geöffnet wird, damit es nicht geschlossen wird?" }, "showIconsChangePasswordUrls": { - "message": "Website Symbole anzeigen und URLs zum Ändern von Passwörtern abrufen" + "message": "Website-Symbole anzeigen und URLs zum Ändern von Passwörtern ermitteln" }, "cardholderName": { "message": "Name des Karteninhabers" @@ -1967,11 +1981,11 @@ "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "Neuen Text senden", + "message": "Neues Text-Send", "description": "Header for new text send" }, "newItemHeaderFileSend": { - "message": "Neue Datei senden", + "message": "Neues Datei-Send", "description": "Header for new file send" }, "editItemHeaderLogin": { @@ -1995,11 +2009,11 @@ "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Textversand bearbeiten", + "message": "Text-Send bearbeiten", "description": "Header for edit text send" }, "editItemHeaderFileSend": { - "message": "Dateiversand bearbeiten", + "message": "Datei-Send bearbeiten", "description": "Header for edit file send" }, "viewItemHeaderLogin": { @@ -2227,7 +2241,7 @@ "message": "Gebe deinen PIN-Code für das Entsperren von Bitwarden ein. Deine PIN-Einstellungen werden zurückgesetzt, wenn du dich vollständig von der Anwendung abmeldest." }, "setPinCode": { - "message": "Du kannst diese PIN verwenden, um Bitwarden zu entsperren. Deine PIN wird zurückgesetzt, wenn du dich vollständig aus der Anwendung abmeldest." + "message": "Du kannst diese PIN verwenden, um Bitwarden zu entsperren. Deine PIN wird zurückgesetzt, wenn du dich einmal vollständig aus der Anwendung abmeldest." }, "pinRequired": { "message": "PIN-Code ist erforderlich." @@ -2645,7 +2659,7 @@ } }, "atRiskChangePrompt": { - "message": "Dein Passwort für diese Website ist gefährdet. $ORGANIZATION$ hat darum gebeten, dass du es änderst.", + "message": "Dein Passwort für diese Website ist gefährdet. $ORGANIZATION$ hat dich aufgefordert, es zu ändern.", "placeholders": { "organization": { "content": "$1", @@ -2655,7 +2669,7 @@ "description": "Notification body when a login triggers an at-risk password change request and the change password domain is known." }, "atRiskNavigatePrompt": { - "message": "$ORGANIZATION$ möchte, dass du dieses Passwort änderst, da es gefährdet ist. Wechsel zu deinen Kontoeinstellungen, um das Passwort zu ändern.", + "message": "$ORGANIZATION$ möchte, dass du dieses Passwort änderst, da es gefährdet ist. Gehe zu deinen Kontoeinstellungen, um das Passwort zu ändern.", "placeholders": { "organization": { "content": "$1", @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Nur der mit $ORGANIZATION$ verknüpfte Organisations-Tresor wird exportiert.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Nur der mit $ORGANIZATION$ verknüpfte Organisations-Tresor wird exportiert. Meine Eintrags-Sammlungen werden nicht eingeschlossen.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Fehler" }, @@ -3752,7 +3784,7 @@ "message": "Anmeldung kann nicht abgeschlossen werden" }, "loginOnTrustedDeviceOrAskAdminToAssignPassword": { - "message": "Du musst dich auf einem vertrauenswürdigen Gerät anmelden oder deinem Administrator bitten, dir ein Passwort zuzuweisen." + "message": "Du musst dich auf einem vertrauenswürdigen Gerät anmelden oder deinen Administrator bitten, dir ein Passwort zuzuweisen." }, "ssoIdentifierRequired": { "message": "SSO-Kennung der Organisation erforderlich." @@ -3834,7 +3866,7 @@ "message": "Fahre zur Sicherheit deines Kontos nur fort, wenn du ein Mitglied dieser Organisation bist, die Kontowiederherstellung aktiviert hast und der unten angezeigte Fingerabdruck mit dem Fingerabdruck der Organisation übereinstimmt." }, "orgTrustWarning1": { - "message": "Diese Organisation hat eine Unternehmensrichtlinie, die dich für die Kontowiederherstellung registriert. Die Registrierung wird es den Administratoren der Organisation erlauben, dein Passwort zu ändern. Fahre nur fort, wenn du diese Organisation kennst und die unten angezeigte Fingerabdruck-Phrase mit der der Organisation übereinstimmt." + "message": "Diese Organisation hat eine Enterprise-Richtlinie, die dich für die Kontowiederherstellung registriert. Die Registrierung ermöglicht es den Organisations-Administratoren, dein Passwort zu ändern. Fahre nur fort, wenn du diese Organisation erkennst und die unten angezeigte Fingerabdruck-Phrase mit dem Fingerabdruck der Organisation übereinstimmt." }, "trustUser": { "message": "Benutzer vertrauen" @@ -4179,10 +4211,10 @@ "message": "Sammlung auswählen" }, "importTargetHintCollection": { - "message": "Wählen Sie diese Option, wenn der importierte Dateiinhalt in eine Sammlung verschoben werden soll" + "message": "Wähle diese Option, wenn der importierte Dateiinhalt in eine Sammlung verschoben werden soll" }, "importTargetHintFolder": { - "message": "Wählen Sie diese Option, wenn der importierte Dateiinhalt in einen Ordner verschoben werden soll" + "message": "Wähle diese Option, wenn der importierte Dateiinhalt in einen Ordner verschoben werden soll" }, "importUnassignedItemsError": { "message": "Die Datei enthält nicht zugewiesene Einträge." @@ -4419,7 +4451,7 @@ "description": "Label indicating the most common import formats" }, "uriMatchDefaultStrategyHint": { - "message": "Die URI-Übereinstimmungserkennung ist die Methode, mit der Bitwarden Auto-Ausfüllen-Vorschläge erkennt.", + "message": "Die URI-Übereinstimmungserkennung ist die Methode, mit der Bitwarden Vorschläge zum automatischen Ausfüllen identifiziert.", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { @@ -5485,10 +5517,10 @@ "message": "Gefährdetes Passwort ändern" }, "changeAtRiskPasswordAndAddWebsite": { - "message": "Dieser Login ist gefährdet und es fehlt eine Website. Fügen Sie eine Website hinzu und ändern Sie das Passwort, um die Sicherheit zu erhöhen." + "message": "Diese Zugangsdaten sind gefährdet und es fehlt eine Website. Füge eine Website hinzu und ändere das Passwort für mehr Sicherheit." }, "missingWebsite": { - "message": "Fehlende Webseite" + "message": "Fehlende Website" }, "settingsVaultOptions": { "message": "Tresoroptionen" @@ -5539,7 +5571,7 @@ "message": "Willkommen in deinem Tresor!" }, "phishingPageTitle": { - "message": "Phishing Webseite" + "message": "Phishing-Website" }, "phishingPageCloseTab": { "message": "Tab schließen" @@ -5548,7 +5580,7 @@ "message": "Weiter" }, "phishingPageLearnWhy": { - "message": "Warum sehen Sie das?" + "message": "Warum wird dir das angezeigt?" }, "hasItemsVaultNudgeBodyOne": { "message": "Einträge für die aktuelle Seite automatisch ausfüllen" @@ -5557,7 +5589,7 @@ "message": "Favoriten-Einträge für einfachen Zugriff" }, "hasItemsVaultNudgeBodyThree": { - "message": "Deinen Tresor nach etwas anderem durchsuchen" + "message": "Durchsuche deinen Tresor nach etwas anderem" }, "newLoginNudgeTitle": { "message": "Spare Zeit mit Auto-Ausfüllen" @@ -5590,16 +5622,16 @@ "message": "Mit Identitäten kannst du lange Registrierungs- oder Kontaktformulare schnell automatisch ausfüllen." }, "newNoteNudgeTitle": { - "message": "Bewahre deine sensiblen Daten sicher auf" + "message": "Halte deine sensiblen Daten sicher" }, "newNoteNudgeBody": { - "message": "Mit Notizen speicherst du sensible Daten wie Bank- oder Versicherungs-Informationen." + "message": "Mit Notizen kannst du sensible Daten wie Bank- oder Versicherungsinformationen sicher speichern." }, "newSshNudgeTitle": { "message": "Entwickler-freundlicher SSH-Zugriff" }, "newSshNudgeBodyOne": { - "message": "Speicher deine Schlüssel und verbinden dich mit dem SSH-Agenten für eine schnelle und verschlüsselte Authentifizierung.", + "message": "Speichere deine Schlüssel und verbinde dich mit dem SSH-Agenten für eine schnelle, verschlüsselte Authentifizierung.", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, @@ -5629,10 +5661,10 @@ "message": "Über diese Einstellung" }, "permitCipherDetailsDescription": { - "message": "Bitwarden wird gespeicherte Login-URIs verwenden, um zu ermitteln, welches Symbol oder welche URL zum Ändern des Passworts verwendet werden soll, um Ihr Erlebnis zu verbessern. Bei der Nutzung dieses Dienstes werden keine Informationen erfasst oder gespeichert." + "message": "Bitwarden verwendet gespeicherte Zugangsdaten-URIs, um zu erkennen, welches Symbol oder welche Passwort-Ändern-URL verwendet werden soll, um dein Erlebnis zu verbessern. Es werden keine Informationen erfasst oder gespeichert, wenn du diesen Dienst nutzt." }, "noPermissionsViewPage": { - "message": "Du hast keine Berechtigung, diese Seite anzuzeigen. Versuche dich mit einem anderen Konto anzumelden." + "message": "Du hast keine Berechtigung, diese Seite anzuzeigen. Versuche, dich mit einem anderen Konto anzumelden." }, "wasmNotSupported": { "message": "WebAssembly wird von deinem Browser nicht unterstützt oder ist nicht aktiviert. WebAssembly wird benötigt, um die Bitwarden-App nutzen zu können.", @@ -5648,10 +5680,10 @@ "message": "Weiter" }, "moreBreadcrumbs": { - "message": "Mehr Breadcrumbs", + "message": "Dateipfad erweitern", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." }, "confirmKeyConnectorDomain": { - "message": "Bestätige Key Connector Domäne" + "message": "Key Connector-Domain bestätigen" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index cce3e0ea39f..139899b10ac 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Επαναφορά αναζήτησης" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Μη έγκυρος κύριος κωδικός πρόσβασης" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Χρόνος Λήξης Vault" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Σφάλμα" }, diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 72c3892af62..d91a33c6796 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -1534,6 +1548,15 @@ "readSecurityKey": { "message": "Read security key" }, + "readingPasskeyLoading": { + "message": "Reading passkey..." + }, + "passkeyAuthenticationFailed": { + "message": "Passkey authentication failed" + }, + "useADifferentLogInMethod": { + "message": "Use a different log in method" + }, "awaitingSecurityKeyInteraction": { "message": "Awaiting security key interaction..." }, @@ -3193,6 +3216,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 43bb17c297f..8dc87aa722a 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 59c4966a48c..cf8b7e08d22 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index d3c6e3556a0..27404fdc5f6 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Restablecer búsqueda" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Contraseña maestra no válida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Tiempo de espera de la caja fuerte" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 5508a1cee72..a4fb0892e46 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Vale ülemparool" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Hoidla ajalõpp" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Viga" }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 93242263dc0..cc6b510f37e 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Pasahitz nagusi baliogabea" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Kutxa gotorraren itxaronaldia" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Akatsa" }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 129f2ee383a..4ec4d513039 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "کلمه عبور اصلی نامعتبر است" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "متوقف شدن گاو‌صندوق" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "خطا" }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 5de1d9fe7e4..5edc9e84c63 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Nollaa haku" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Virheellinen pääsalasana" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Holvin aikakatkaisu" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Virhe" }, diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 600abfb2d4e..2afa450a7d0 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Hindi wasto ang master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Mali" }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 765ebff53c5..c74f0248693 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Réinitialiser la recherche" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Mot de passe principal invalide" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Délai d'expiration du coffre" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Erreur" }, diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index c2573ea6bfa..421adf16a2f 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Contrasinal mestre non válido" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Tempo de espera da caixa forte" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Erro" }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 38fe3618610..affba06eafd 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "אפס חיפוש" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "סיסמה ראשית שגויה" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "פסק זמן לכספת" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "שגיאה" }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 1575543aef3..323d72f802d 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "खोज रीसेट करें" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "अमान्य मास्टर पासवर्ड" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "वॉल्ट मध्यांतर" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "एरर" }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 4f67de34071..34c114c214d 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Ponovno postavljanje pretraživanja" }, - "archive": { - "message": "Arhiviraj" + "archiveNoun": { + "message": "Arhiva", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arhiviraj", + "description": "Verb" }, "unarchive": { "message": "Poništi arhiviranje" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Neispravna glavna lozinka" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Nevažeća glavna lozinka. Provjeri je li tvoja adresa e-pošta ispravna i je li račun kreiran na $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Istek trezora" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Izvezt će se samo trezor organizacije povezan s $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Izvezt će se samo trezor organizacije povezan s $ORGANIZATION$. Zbirka mojih stavki neće biti uključena.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Pogreška" }, diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 864580a64b0..2cafe38fab4 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Keresés visszaállítása" }, - "archive": { - "message": "Archívum" + "archiveNoun": { + "message": "Archívum", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archívum", + "description": "Verb" }, "unarchive": { "message": "Visszavétel archívumból" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Hibás mesterjelszó" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "A mesterjelszó érvénytelen. Erősítsük meg, hogy email cím helyes és a fiók létrehozásának helye: $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Széf időkifutás" }, @@ -1654,7 +1668,7 @@ "message": "Automat kitöltés bekapcsolása" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "Automatikus kitöltési javaslatok megjelenítése űrlapmezőknél" }, "showInlineMenuIdentitiesLabel": { "message": "Az identitások megjelenítése javaslatként" @@ -1663,10 +1677,10 @@ "message": "A kártyák megjelenítése javaslatként" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "Javaslatok megjelenítése a az ikon kiválasztásakor" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "Minden bejelentkezett fiókra vonatkozik." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Az ütközések elkerülése érdekében kapcsoljuk ki a böngésző beépített jelszókezelő beállításait." @@ -1679,7 +1693,7 @@ "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { - "message": "When field is selected (on focus)", + "message": "Amikor a mező kiválasztásra kerül (fókuszoláskor)", "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { @@ -1687,7 +1701,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "Automatikus kitöltés az oldal betöltésénél" }, "enableAutoFillOnPageLoad": { "message": "Automatikus kitöltés engedélyezése oldal betöltéskor" @@ -1699,7 +1713,7 @@ "message": "Az oldalbetöltésnél automatikus kitöltést a feltört vagy nem megbízhatató weboldalak kihasználhatják." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Bővebben a kockázatokról" }, "learnMoreAboutAutofill": { "message": "További információk az automatikus kitöltésről" @@ -2423,7 +2437,7 @@ "message": "Az új mesterjelszó nem felel meg a szabály követelményeknek." }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "Tanácsokat, bejelentéseket, kutatási lehetőségeket kaphat a Bitwarden-től a postaládájába." }, "unsubscribe": { "message": "Leiratkozás" @@ -2459,10 +2473,10 @@ "message": "Ok" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Hozzáférési Token Frissítés Hiba" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Nem található token vagy API kulcs. Próbáljon meg ki-, majd újra bejelentkezni." }, "desktopSyncVerificationTitle": { "message": "Asztali szinkronizálás ellenőrzés" @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Csak a $ORGANIZATION$ szervezetehez kapcsolódó szervezeti széf kerül exportálásra.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Csak a $ORGANIZATION$ szervezethez kapcsolódó szervezeti széf kerül exportálásra. A saját elem gyűjtemények nem lesznek benne.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Hiba" }, @@ -3583,7 +3615,7 @@ "message": "A szervezeti szabályzat bekapcsolta az automatikus kitöltést az oldalbetöltéskor." }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "Válasszon ki egy elemet a képernyőről, használja a $COMMAND$ kombinációt, vagy tekintse meg a többi lehetőséget a beállításokban.", "placeholders": { "command": { "content": "$1", @@ -3592,7 +3624,7 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "Válasszon ki egy elemet a képernyőről, vagy tekintse meg a többi lehetőséget a beállításokban." }, "gotIt": { "message": "Rendben" @@ -3601,10 +3633,10 @@ "message": "Automatikus kitöltés beállítások" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "Automatikus kitöltés gyorselérés" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "Billentyűparancs változtatás" }, "autofillKeyboardManagerShortcutsLabel": { "message": "Bullenytűparancsok kezelése" @@ -3971,17 +4003,17 @@ "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Oldalnavigáció váltás" }, "skipToContent": { "message": "Ugrás a tartalomra" }, "bitwardenOverlayButton": { - "message": "Bitwarden autofill menu button", + "message": "Bitwarden automatikus kitöltés menü gomb", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden autofill menu", + "message": "Bitwarden automatikus kitöltés menü váltás", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { @@ -3989,7 +4021,7 @@ "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { - "message": "Unlock your account to view matching logins", + "message": "Az összeillő belépések megtekintéséhez oldja fel fiókját", "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { @@ -4057,7 +4089,7 @@ "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden autofill menu available. Press the down arrow key to select.", + "message": "Bitwarden automatikus kitöltés menü elérhető. Nyomja meg a lefele nyilat a kiválasztáshoz.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -4143,7 +4175,7 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "Hiba a Duo szolgáltatáshoz való kapcsolódáskor. Használjon másféle kétlépcsős bejelentkezést, vagy keresse fel a Duo ügyfélszolgálatot." }, "duoRequiredForAccount": { "message": "A fiókhoz kétlépcsős DUO bejelentkezés szükséges." @@ -4439,27 +4471,27 @@ "description": "Advanced option placeholder for uri option component" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "Továbblépés a böngésző beállításokhoz?", "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": "Továbblépés a Segítség Központba?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "A böngésző automatikus kitöltés és jelszókezelési beállításainak módosítása.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "Megtekintheti vagy beállíthatja a bővítmény billentyűparancsokat a böngésző beállításoknál.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "A böngésző automatikus kitöltési és jelszókezelési beállításainak módosítása.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "Megtekintheti vagy beállíthatja a bővítmény billentyűparancsokat a böngésző beállításoknál.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { @@ -4639,7 +4671,7 @@ "message": "Nincsenek másolandó értékek." }, "assignToCollections": { - "message": "Assign to collections" + "message": "Hozzárendelés gyűjteményhez" }, "copyEmail": { "message": "Email cím másolása" @@ -4702,7 +4734,7 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "Be nem mappázott elemek" }, "itemDetails": { "message": "Elem részletek" @@ -4711,7 +4743,7 @@ "message": "Elem neve" }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "A szervezet deaktiválásra került" }, "owner": { "message": "Tulajdonos" @@ -4721,7 +4753,7 @@ "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." + "message": "A deaktivált szervezetek elemeit nem lehet elérni. Keresse fel további segítségért a szervezet tulajdonosát." }, "additionalInformation": { "message": "További információ" @@ -4949,7 +4981,7 @@ "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "Hozzárendelés" }, "bulkCollectionAssignmentDialogDescriptionSingular": { "message": "Csak az ezekhez a gyűjteményekhez hozzáféréssel rendelkező szervezeti tagok láthatják az elemet." @@ -4958,7 +4990,7 @@ "message": "Csak az ezekhez a gyűjteményekhez hozzáféréssel rendelkező szervezeti tagok láthatják az elemeket." }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "$TOTAL_COUNT$ elemeket jelölt ki. Nem frissítheti a $READONLY_COUNT$ részét, mert nem rendelkezik szerkesztési jogosultsággal.", "placeholders": { "total_count": { "content": "$1", @@ -5056,13 +5088,13 @@ } }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "A hozzárendeléshez jelöljön ki gyűjteményeket" }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "1 elem véglegesen áthelyezésre kerül a szervezethez. Többé nem Önhöz fog tartozni az elem." }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ elemek véglegesen áthelyezésre kerülnek a kiválasztott szervezethez. Többé nem Önhöz fognak tartozni az elemek.", "placeholders": { "personal_items_count": { "content": "$1", @@ -5071,7 +5103,7 @@ } }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "message": "Ez az 1 elem véglegesen áthelyezésre kerül a $ORG$ szervezethez. Többé nem az Öné lesz az elem.", "placeholders": { "org": { "content": "$1", @@ -5080,7 +5112,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ elem véglegesen áthelyezésre kerül a $ORG$ szervezethez. Többé nem az Öné lesz az elem.", "placeholders": { "personal_items_count": { "content": "$1", @@ -5093,13 +5125,13 @@ } }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Sikerült a gyűjteményhez való hozzárendelés" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "Nem választott ki semmit." }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "Elemek áthelyezve a $ORGNAME$ szervezethez", "placeholders": { "orgname": { "content": "$1", @@ -5108,7 +5140,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "Elem áthelyezve a $ORGNAME$ szervezethez", "placeholders": { "orgname": { "content": "$1", @@ -5269,7 +5301,7 @@ "message": "Feloldás biometrikusan" }, "authenticating": { - "message": "Authenticating" + "message": "Hitelesítés" }, "fillGeneratedPassword": { "message": "Generált jelszó kitöltés", diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index b38b6f05628..80886711095 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Atur ulang pencarian" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Sandi utama tidak valid" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Batas Waktu Brankas" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Galat" }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index df4411ee42b..c9a51f416e4 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Svuota ricerca" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Password principale errata" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Timeout cassaforte" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Errore" }, diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 5305a265781..0fa768e3716 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "マスターパスワードが間違っています" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "保管庫のタイムアウト" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "エラー" }, diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index b759d674cca..13f3da81404 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -162,7 +162,7 @@ "message": "Copy passport number" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "დააკოპირეთ ლიცენზიის ნომერი" }, "copyPrivateKey": { "message": "Copy private key" @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "შეცდომა" }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 78a49021a0c..dcd86eb4337 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 1311a97df68..16e6cc2a631 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "ಅಮಾನ್ಯ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "ವಾಲ್ಟ್ ಕಾಲಾವಧಿ" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 06611be0282..9b80dae4d7c 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "잘못된 마스터 비밀번호" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "보관함 시간 제한" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "오류" }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 464fa5aae92..478164a7f9c 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Neteisingas pagrindinis slaptažodis" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Atsijungta nuo saugyklos" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Klaida" }, diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 99edb486d9d..f9a8c3fba2b 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Atiestatīt meklēšanu" }, - "archive": { - "message": "Arhivēt" + "archiveNoun": { + "message": "Arhīvs", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arhivēt", + "description": "Verb" }, "unarchive": { "message": "Atcelt arhivēšanu" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Nederīga galvenā parole" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Nederīga galvenā parole. Jāpārliecinās, ka e-pasta adrese ir pareiza un konts tika izveidots $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Glabātavas noildze" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Kļūda" }, diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index efe18c96a59..cd99f32714e 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "അസാധുവായ പ്രാഥമിക പാസ്‌വേഡ്" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "വാൾട് ടൈംഔട്ട്" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 16ac31ff599..e639bae2662 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "अवैध मुख्य पासवर्ड" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 78a49021a0c..dcd86eb4337 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index a23fd7fe4c1..b01d5a5ebbf 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Ugyldig hovedpassord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Tidsavbrudd i hvelvet" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Feil" }, diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 78a49021a0c..dcd86eb4337 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 2562b7a1d4c..62f81cf0897 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Zoekopdracht resetten" }, - "archive": { - "message": "Archiveren" + "archiveNoun": { + "message": "Archief", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archiveren", + "description": "Verb" }, "unarchive": { "message": "Dearchiveren" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Ongeldig hoofdwachtwoord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ongeldig hoofdwachtwoord. Check of je e-mailadres klopt en of je account is aangemaakt op $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Time-out van de kluis" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Alleen de organisatiekluis die gekoppeld is aan $ORGANIZATION$ wordt geëxporteerd.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Exporteert alleen de organisatiekluis van $ORGANIZATION$. Geen persoonlijke kluis-items.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Fout" }, @@ -3432,10 +3464,10 @@ "message": "Premium-abonnement vereist" }, "organizationIsDisabled": { - "message": "Organisatie is uitgeschakeld." + "message": "Organisatie opgeschort." }, "disabledOrganizationFilterError": { - "message": "Je kunt uitgeschakelde items in een organisatie niet benaderen. Neem contact op met de eigenaar van je organisatie voor hulp." + "message": "Je kunt items in opgeschorte organisaties niet benaderen. Neem contact op met de eigenaar van je organisatie voor hulp." }, "loggingInTo": { "message": "Inloggen op $DOMAIN$", diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 78a49021a0c..dcd86eb4337 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 78a49021a0c..dcd86eb4337 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index f24e790c9ad..8d96ccaffc6 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Zresetuj wyszukiwanie" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archiwum", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archiwizuj", + "description": "Verb" }, "unarchive": { "message": "Usuń z archiwum" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Hasło główne jest nieprawidłowe" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Blokowanie sejfu" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Błąd" }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 2d7dd1e42a4..746c04bdf0f 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Senha mestra inválida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Cofre - tempo esgotado" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Erro" }, @@ -3640,7 +3672,7 @@ "message": "Lembrar deste dispositivo para permanecer conectado" }, "manageDevices": { - "message": "Manage devices" + "message": "Gerenciar dispositivos" }, "currentSession": { "message": "Current session" @@ -3683,7 +3715,7 @@ "message": "Needs approval" }, "devices": { - "message": "Devices" + "message": "Dispositivos" }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index acc5b5332f9..345be1766de 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Repor pesquisa" }, - "archive": { - "message": "Arquivar" + "archiveNoun": { + "message": "Arquivo", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arquivar", + "description": "Verb" }, "unarchive": { "message": "Desarquivar" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Palavra-passe mestra inválida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Palavra-passe mestra inválida. Confirme se o seu e-mail está correto e se a sua conta foi criada em $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Tempo limite do cofre" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Apenas o cofre da organização associado a $ORGANIZATION$ será exportado.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Apenas o cofre da organização associado a $ORGANIZATION$ será exportado. As coleções dos meus itens não serão incluídas.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Erro" }, diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index d184460e293..2dcf2fb6181 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Parolă principală incorectă" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Expirare seif" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Eroare" }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 17133350e3f..593e2b5d672 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Сбросить поиск" }, - "archive": { - "message": "Архив" + "archiveNoun": { + "message": "Архив", + "description": "Noun" + }, + "archiveVerb": { + "message": "Архивировать", + "description": "Verb" }, "unarchive": { "message": "Разархивировать" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Неверный мастер-пароль" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Неверный мастер-пароль. Подтвердите, что ваш адрес email указан верно и ваш аккаунт был создан на $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Тайм-аут хранилища" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Будет экспортировано только хранилище организации, связанное с $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Будет экспортировано только хранилище организации, связанное с $ORGANIZATION$. Коллекции Мои элементы включены не будут.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Ошибка" }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 2fd8f53e148..8218a5d8787 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "වලංගු නොවන ප්රධාන මුරපදය" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "සුරක්ෂිතාගාරය වේලාව" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index d0e143cce4a..cfc1404e9bf 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Resetovať vyhľadávanie" }, - "archive": { - "message": "Archivovať" + "archiveNoun": { + "message": "Archív", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archivovať", + "description": "Verb" }, "unarchive": { "message": "Zrušiť archiváciu" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Neplatné hlavné heslo" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Neplatné hlavné heslo. Potvrďte, že váš e-mail je správny a účet bol vytvorený na $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Časový limit pre trezor" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Exportuje sa len trezor organizácie spojený s $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Exportuje sa len trezor organizácie spojený s $ORGANIZATION$. Moje zbierky položiek nebudú zahrnuté.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Chyba" }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 81b1a6bb52c..23327bbb1ae 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Napačno glavno geslo" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Zakleni trezor, ko preteče toliko časa:" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Napaka" }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 269ebd41bdd..a915463799a 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Ресетовати претрагу" }, - "archive": { - "message": "Архива" + "archiveNoun": { + "message": "Архива", + "description": "Noun" + }, + "archiveVerb": { + "message": "Архива", + "description": "Verb" }, "unarchive": { "message": "Врати из архиве" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Погрешна главна лозинка" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Неважећа главна лозинка. Потврдите да је ваш имејл тачан и ваш рачун је креиран на $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Тајмаут сефа" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Извешће се само сеф организације повезана са $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Извешће се само сеф организације повезан са $ORGANIZATION$. Колекције мојих предмета неће бити укључене.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Грешка" }, diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 8b0263bf15a..fb83fdf22b6 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Nollställ sökning" }, - "archive": { - "message": "Arkivera" + "archiveNoun": { + "message": "Arkiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arkivera", + "description": "Verb" }, "unarchive": { "message": "Packa upp" @@ -563,19 +568,19 @@ "message": "Inga objekt i arkivet" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Arkiverade objekt kommer att visas här och kommer att uteslutas från allmänna sökresultat och förslag för autofyll." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Objekt skickat till arkiv" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Objekt borttaget från arkiv" }, "archiveItem": { "message": "Arkivera objekt" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Arkiverade objekt är exkluderade från allmänna sökresultat och förslag för autofyll. Är du säker på att du vill arkivera detta objekt?" }, "edit": { "message": "Redigera" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Ogiltigt huvudlösenord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ogiltigt huvudlösenord. Bekräfta att din e-postadress är korrekt och ditt konto skapades på $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Valvets tidsgräns" }, @@ -1967,11 +1981,11 @@ "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "New Text Send", + "message": "Ny textsändning", "description": "Header for new text send" }, "newItemHeaderFileSend": { - "message": "New File Send", + "message": "Ny filsändning", "description": "Header for new file send" }, "editItemHeaderLogin": { @@ -1995,11 +2009,11 @@ "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Edit Text Send", + "message": "Redigera textsändning", "description": "Header for edit text send" }, "editItemHeaderFileSend": { - "message": "Edit File Send", + "message": "Redigera filsändning", "description": "Header for edit file send" }, "viewItemHeaderLogin": { @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Endast organisationsvalvet som är associerat med $ORGANIZATION$ kommer att exporteras.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Endast organisationsvalvet som associeras med $ORGANIZATION$ kommer att exporteras. Mina objektsamlingar kommer inte att inkluderas.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Fel" }, diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index 8d2199db6ca..8d1f1ccf87f 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "தேடலை மீட்டமை" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "தவறான முதன்மை கடவுச்சொல்" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "வால்ட் காலக்கெடு" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "பிழை" }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 78a49021a0c..dcd86eb4337 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Vault timeout" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 61f97564f6a..064b70c26ec 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Reset search" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "รหัสผ่านหลักไม่ถูกต้อง" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "ระยะเวลาล็อกตู้เซฟ" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 0b65ae7d476..470589c286d 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -362,7 +362,7 @@ "message": "Ücretsiz Bitwarden Aile" }, "freeBitwardenFamiliesPageDesc": { - "message": "Ücretsiz Bitwarden Aile Paketi’nden faydalanmaya hak kazandınız. Bu teklifi bugün web uygulaması üzerinden kullanın." + "message": "Ücretsiz Bitwarden Aileleri için uygunsun. Bu teklifi bugün web uygulamasında kullan." }, "version": { "message": "Sürüm" @@ -550,32 +550,37 @@ "resetSearch": { "message": "Aramayı sıfırla" }, - "archive": { - "message": "Arşivle" + "archiveNoun": { + "message": "Arşiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arşivle", + "description": "Verb" }, "unarchive": { "message": "Arşivden çıkar" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Arşivdeki kayıtlar" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Arşivde kayıt yok" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Arşivlenmiş kayıtlar burada görünecek ve genel arama sonuçlarından ile otomatik doldurma önerilerinden hariç tutulacaktır." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Kayıt arşive gönderildi" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Kayıt arşivden çıkarıldı" }, "archiveItem": { - "message": "Archive item" + "message": "Kaydı arşivle" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Arşivlenmiş kayıtlar genel arama sonuçları ve otomatik doldurma önerilerinden hariç tutulur. Bu kaydı arşivlemek istediğine emin misin?" }, "edit": { "message": "Düzenle" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Geçersiz ana parola" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ana parola geçersiz. E-posta adresinizin doğru olduğunu ve hesabınızın $HOST$ üzerinde oluşturulduğunu kontrol edin.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Kasa zaman aşımı" }, @@ -875,13 +889,13 @@ "message": "Mevcut web sayfasındaki kimlik doğrulayıcı QR kodunu tarayın" }, "totpHelperTitle": { - "message": "2 adımlı doğrulamayı sorunsuz hale getirin" + "message": "2 adımlı doğrulamayı sorunsuz hale getir" }, "totpHelper": { - "message": "Bitwarden, 2 adımlı doğrulama kodlarını saklayabilir ve otomatik olarak doldurabilir. Anahtarı kopyalayıp bu alana yapıştırın." + "message": "Bitwarden 2 adımlı doğrulama kodlarını saklayabilir ve doldurabilir. Anahtarı bu alana kopyala ve yapıştır." }, "totpHelperWithCapture": { - "message": "Bitwarden, iki adımlı doğrulama kodlarını saklayabilir ve otomatik olarak doldurabilir. Bu web sitesinin doğrulayıcı QR kodunun ekran görüntüsünü almak için kamera simgesini seçin veya anahtarı bu alana kopyalayıp yapıştırın." + "message": "Bitwarden 2 adımlı doğrulama kodlarını saklayabilir ve doldurabilir. Bu web sitesinin kimlik doğrulayıcı QR kodunun ekran görüntüsünü almak için kamera simgesini seç veya anahtarı bu alana kopyala ve yapıştır." }, "learnMoreAboutAuthenticators": { "message": "Kimlik doğrulayıcılar hakkında bilgi alın" @@ -1592,7 +1606,7 @@ "message": "Şirket içinde barındırılan ortam" }, "selfHostedBaseUrlHint": { - "message": "Yerel sunucunuzda barındırılan Bitwarden kurulumunuzun temel URL’sini belirtin. Örnek: https://bitwarden.sirketiniz.com" + "message": "Şirket içinde barındırılan Bitwarden kurulumunun temel URL’sini belirt. Örnek: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { "message": "İleri düzey yapılandırma için her hizmetin taban URL'sini bağımsız olarak belirleyebilirsiniz." @@ -2558,7 +2572,7 @@ "message": "Bir kuruluş ilkesi, kayıtları kişisel kasanıza içe aktarmayı engelledi." }, "restrictCardTypeImport": { - "message": "Kart öge türleri içe aktarılamıyor" + "message": "Kart kayıt türleri içe aktarılamıyor" }, "restrictCardTypeImportDesc": { "message": "1 veya daha fazla kuruluş tarafından belirlenen bir ilke, kasalarınıza kart aktarmanızı engelliyor." @@ -2689,14 +2703,14 @@ "message": "Risk altındaki parolaları inceleyin" }, "reviewAtRiskLoginsSlideDesc": { - "message": "Organizasyonunuzun parolaları zayıf, tekrar kullanılmış ve/veya açığa çıkmış olduğu için risk altındadır.", + "message": "Kuruluş parolaların zayıf, yeniden kullanılmış ve/veya ele geçirilmiş olduğundan risk altındadır.", "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Risk altındaki hesap listesinin illüstrasyonu." }, "generatePasswordSlideDesc": { - "message": "Risk altındaki sitede Bitwarden otomatik doldurma menüsü ile hızlıca güçlü ve benzersiz bir parola oluşturun.", + "message": "Riskli sitede Bitwarden otomatik doldurma menüsünü kullanarak hızlıca güçlü ve benzersiz bir parola oluştur.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAltPeriod": { @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Yalnızca $ORGANIZATION$ ile ilişkili kuruluş kasası dışa aktarılacaktır.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Yalnızca $ORGANIZATION$ ile ilişkili kuruluş kasası dışa aktarılacaktır. Kayıt koleksiyonlarım dahil edilmeyecektir.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Hata" }, @@ -5629,7 +5661,7 @@ "message": "Bu ayar hakkında" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden, deneyimini iyileştirmek için hangi simgenin veya parola değiştirme URL’sinin kullanılacağını belirlemek amacıyla kaydedilmiş hesap URI’lerini kullanır. Bu hizmeti kullandığında hiçbir bilgi toplanmaz veya kaydedilmez." }, "noPermissionsViewPage": { "message": "Bu sayfayı görüntüleme izniniz yok. Farklı bir hesapla giriş yapmayı deneyin." diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index f088e610051..fb64a91330d 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Скинути пошук" }, - "archive": { - "message": "Архівувати" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Видобути" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Неправильний головний пароль" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Час очікування сховища" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Помилка" }, diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 06920433037..8b69fdc2512 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "Đặt lại tìm kiếm" }, - "archive": { - "message": "Lưu trữ" + "archiveNoun": { + "message": "Lưu trữ", + "description": "Noun" + }, + "archiveVerb": { + "message": "Lưu trữ", + "description": "Verb" }, "unarchive": { "message": "Hủy lưu trữ" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "Mật khẩu chính không hợp lệ" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Mật khẩu chính không hợp lệ. Xác nhận email của bạn là chính xác và tài khoản được tạo trên $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "Đóng kho sau" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Chỉ kho lưu trữ tổ chức liên kết với $ORGANIZATION$ sẽ được xuất.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Chỉ kho lưu trữ tổ chức liên kết với $ORGANIZATION$ được xuất. Bộ sưu tập mục của tôi sẽ không được bao gồm.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "Lỗi" }, diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index eb3cf1aa901..9ced7b1efd7 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "重置搜索" }, - "archive": { - "message": "归档" + "archiveNoun": { + "message": "归档", + "description": "Noun" + }, + "archiveVerb": { + "message": "归档", + "description": "Verb" }, "unarchive": { "message": "取消归档" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "无效的主密码" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "无效的主密码。请确认您的电子邮箱正确无误,以及您的账户是在 $HOST$ 上创建的。", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "密码库超时时间" }, @@ -1134,7 +1148,7 @@ "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "选择 $ITEMTYPE$,$ITEMNAME$", + "message": "选择 $ITEMNAME$ 中的 $ITEMTYPE$", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库。", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库,不包括我的项目集合。", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "错误" }, @@ -4622,7 +4654,7 @@ } }, "copyFieldCipherName": { - "message": "复制 $CIPHERNAME$ 的 $FIELD$", + "message": "复制 $CIPHERNAME$ 中的 $FIELD$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index f5801fb2c7d..5f6defcfe24 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -550,8 +550,13 @@ "resetSearch": { "message": "重設搜尋" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" @@ -728,6 +733,15 @@ "invalidMasterPassword": { "message": "無效的主密碼" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "vaultTimeout": { "message": "密碼庫逾時時間" }, @@ -3193,6 +3207,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "error": { "message": "錯誤" }, @@ -3541,7 +3573,7 @@ "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." }, "device": { - "message": "Device" + "message": "裝置" }, "loginStatus": { "message": "Login status" @@ -3640,7 +3672,7 @@ "message": "記住此裝置來讓未來的登入體驗更簡易" }, "manageDevices": { - "message": "Manage devices" + "message": "管理裝置" }, "currentSession": { "message": "Current session" @@ -3683,7 +3715,7 @@ "message": "Needs approval" }, "devices": { - "message": "Devices" + "message": "裝置" }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", @@ -3704,7 +3736,7 @@ "message": "Time" }, "deviceType": { - "message": "Device Type" + "message": "裝置類型" }, "loginRequest": { "message": "Login request" @@ -5434,10 +5466,10 @@ "message": "擴充套件寬度" }, "wide": { - "message": "寬度" + "message": "寬" }, "extraWide": { - "message": "更寬" + "message": "超寬" }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.html b/apps/browser/src/auth/popup/account-switching/current-account.component.html index c16abdadf29..2e2440f6258 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.html +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.html @@ -12,7 +12,6 @@ [color]="currentAccount.avatarColor" size="small" aria-hidden="true" - class="[&>img]:tw-block" > diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts index 7bb12fc260d..99d2c83283e 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts @@ -160,7 +160,7 @@ export class AccountSwitcherService { throwError(() => new Error(AccountSwitcherService.incompleteAccountSwitchError)), }), ), - ).catch((err) => { + ).catch((err): any => { if ( err instanceof Error && err.message === AccountSwitcherService.incompleteAccountSwitchError diff --git a/apps/browser/src/auth/popup/login/extension-login-component.service.ts b/apps/browser/src/auth/popup/login/extension-login-component.service.ts index 37d74616391..621c7d74876 100644 --- a/apps/browser/src/auth/popup/login/extension-login-component.service.ts +++ b/apps/browser/src/auth/popup/login/extension-login-component.service.ts @@ -68,4 +68,18 @@ export class ExtensionLoginComponentService showBackButton(showBackButton: boolean): void { this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData({ showBackButton }); } + + /** + * Enable passkey login support for chromium-based browsers only. + * Neither Firefox nor safari support overriding the relying party ID in an extension. + * + * https://github.com/w3c/webextensions/issues/238 + * + * Tracking links: + * https://bugzilla.mozilla.org/show_bug.cgi?id=1956484 + * https://developer.apple.com/forums/thread/774351 + */ + isLoginWithPasskeySupported(): boolean { + return this.platformUtilsService.isChromium(); + } } diff --git a/apps/browser/src/auth/services/auth-status-badge-updater.service.ts b/apps/browser/src/auth/services/auth-status-badge-updater.service.ts index 4205ebc665d..4f239e54939 100644 --- a/apps/browser/src/auth/services/auth-status-badge-updater.service.ts +++ b/apps/browser/src/auth/services/auth-status-badge-updater.service.ts @@ -17,8 +17,8 @@ export class AuthStatusBadgeUpdaterService { private accountService: AccountService, private authService: AuthService, ) { - this.accountService.activeAccount$ - .pipe( + this.badgeService.setState(StateName, (_tab) => + this.accountService.activeAccount$.pipe( switchMap((account) => account ? this.authService.authStatusFor$(account.id) @@ -27,30 +27,36 @@ export class AuthStatusBadgeUpdaterService { mergeMap(async (authStatus) => { switch (authStatus) { case AuthenticationStatus.LoggedOut: { - await this.badgeService.setState(StateName, BadgeStatePriority.High, { - icon: BadgeIcon.LoggedOut, - backgroundColor: Unset, - text: Unset, - }); - break; + return { + priority: BadgeStatePriority.High, + state: { + icon: BadgeIcon.LoggedOut, + backgroundColor: Unset, + text: Unset, + }, + }; } case AuthenticationStatus.Locked: { - await this.badgeService.setState(StateName, BadgeStatePriority.High, { - icon: BadgeIcon.Locked, - backgroundColor: Unset, - text: Unset, - }); - break; + return { + priority: BadgeStatePriority.High, + state: { + icon: BadgeIcon.Locked, + backgroundColor: Unset, + text: Unset, + }, + }; } case AuthenticationStatus.Unlocked: { - await this.badgeService.setState(StateName, BadgeStatePriority.Low, { - icon: BadgeIcon.Unlocked, - }); - break; + return { + priority: BadgeStatePriority.Low, + state: { + icon: BadgeIcon.Unlocked, + }, + }; } } }), - ) - .subscribe(); + ), + ); } } diff --git a/apps/browser/src/auth/services/new-device-verification/extension-new-device-verification-component.service.spec.ts b/apps/browser/src/auth/services/new-device-verification/extension-new-device-verification-component.service.spec.ts new file mode 100644 index 00000000000..7c91cae3fcb --- /dev/null +++ b/apps/browser/src/auth/services/new-device-verification/extension-new-device-verification-component.service.spec.ts @@ -0,0 +1,21 @@ +import { ExtensionNewDeviceVerificationComponentService } from "./extension-new-device-verification-component.service"; + +describe("ExtensionNewDeviceVerificationComponentService", () => { + let sut: ExtensionNewDeviceVerificationComponentService; + + beforeEach(() => { + sut = new ExtensionNewDeviceVerificationComponentService(); + }); + + it("should instantiate the service", () => { + expect(sut).not.toBeFalsy(); + }); + + describe("showBackButton()", () => { + it("should return false", () => { + const result = sut.showBackButton(); + + expect(result).toBe(false); + }); + }); +}); diff --git a/apps/browser/src/auth/services/new-device-verification/extension-new-device-verification-component.service.ts b/apps/browser/src/auth/services/new-device-verification/extension-new-device-verification-component.service.ts new file mode 100644 index 00000000000..05e60fc8dad --- /dev/null +++ b/apps/browser/src/auth/services/new-device-verification/extension-new-device-verification-component.service.ts @@ -0,0 +1,13 @@ +import { + DefaultNewDeviceVerificationComponentService, + NewDeviceVerificationComponentService, +} from "@bitwarden/auth/angular"; + +export class ExtensionNewDeviceVerificationComponentService + extends DefaultNewDeviceVerificationComponentService + implements NewDeviceVerificationComponentService +{ + showBackButton() { + return false; + } +} diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index 52720b1f9f5..912d9657124 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -35,7 +35,7 @@ interface NotificationQueueMessage { } type ChangePasswordNotificationData = { - cipherId: CipherView["id"]; + cipherIds: CipherView["id"][]; newPassword: string; }; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 032baf2e32b..f9e2e1c534f 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -133,19 +133,11 @@ describe("NotificationBackground", () => { expect(cipherView.name).toEqual("example.com"); expect(cipherView.login).toEqual({ - autofillOnPageLoad: null, - fido2Credentials: null, + fido2Credentials: [], password: message.password, - passwordRevisionDate: null, - totp: null, uris: [ { - _canLaunch: null, - _domain: null, - _host: null, - _hostname: null, _uri: message.uri, - match: null, }, ], username: message.username, @@ -289,7 +281,6 @@ describe("NotificationBackground", () => { let tab: chrome.tabs.Tab; let sender: chrome.runtime.MessageSender; let getEnableAddedLoginPromptSpy: jest.SpyInstance; - let getEnableChangedPasswordPromptSpy: jest.SpyInstance; let pushAddLoginToQueueSpy: jest.SpyInstance; let pushChangePasswordToQueueSpy: jest.SpyInstance; let getAllDecryptedForUrlSpy: jest.SpyInstance; @@ -306,10 +297,7 @@ describe("NotificationBackground", () => { notificationBackground as any, "getEnableAddedLoginPrompt", ); - getEnableChangedPasswordPromptSpy = jest.spyOn( - notificationBackground as any, - "getEnableChangedPasswordPrompt", - ); + pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue"); pushChangePasswordToQueueSpy = jest.spyOn( notificationBackground as any, @@ -368,24 +356,6 @@ describe("NotificationBackground", () => { expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); - it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => { - const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData; - activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); - getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); - getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false); - getAllDecryptedForUrlSpy.mockResolvedValueOnce([ - mock({ login: { username: "test", password: "oldPassword" } }), - ]); - - await notificationBackground.triggerAddLoginNotification(data, tab); - - expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); - expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); - expect(getEnableChangedPasswordPromptSpy).toHaveBeenCalled(); - expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); - expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); - }); - it("skips attempting to change the password for an existing login if the password has not changed", async () => { const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData; activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); @@ -445,37 +415,12 @@ describe("NotificationBackground", () => { sender.tab, ); }); - - it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => { - const data: ModifyLoginCipherFormData = { - ...mockModifyLoginCipherFormData, - username: "tEsT", - }; - - activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); - getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true); - getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true); - getAllDecryptedForUrlSpy.mockResolvedValueOnce([ - mock({ - id: "cipher-id", - login: { username: "test", password: "oldPassword" }, - }), - ]); - - await notificationBackground.triggerAddLoginNotification(data, tab); - - expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( - "cipher-id", - "example.com", - data.password, - sender.tab, - ); - }); }); describe("bgTriggerChangedPasswordNotification message handler", () => { let tab: chrome.tabs.Tab; let sender: chrome.runtime.MessageSender; + let getEnableChangedPasswordPromptSpy: jest.SpyInstance; let pushChangePasswordToQueueSpy: jest.SpyInstance; let getAllDecryptedForUrlSpy: jest.SpyInstance; const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = { @@ -488,6 +433,11 @@ describe("NotificationBackground", () => { beforeEach(() => { tab = createChromeTabMock(); sender = mock({ tab }); + getEnableChangedPasswordPromptSpy = jest.spyOn( + notificationBackground as any, + "getEnableChangedPasswordPrompt", + ); + pushChangePasswordToQueueSpy = jest.spyOn( notificationBackground as any, "pushChangePasswordToQueue", @@ -495,6 +445,40 @@ describe("NotificationBackground", () => { getAllDecryptedForUrlSpy = jest.spyOn(cipherService, "getAllDecryptedForUrl"); }); + afterEach(() => { + getEnableChangedPasswordPromptSpy.mockRestore(); + pushChangePasswordToQueueSpy.mockRestore(); + getAllDecryptedForUrlSpy.mockRestore(); + }); + + it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => { + const data: ModifyLoginCipherFormData = { + ...mockModifyLoginCipherFormData, + }; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false); + getAllDecryptedForUrlSpy.mockResolvedValueOnce([ + mock({ login: { username: "test", password: "oldPassword" } }), + ]); + + await notificationBackground.triggerChangedPasswordNotification(data, tab); + + expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); + }); + + it("skips attempting to add the change password message to the queue if the user is logged out", async () => { + const data: ModifyLoginCipherFormData = { + ...mockModifyLoginCipherFormData, + uri: "https://example.com", + }; + + activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut); + + await notificationBackground.triggerChangedPasswordNotification(data, tab); + + expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); + }); + it("skips attempting to add the change password message to the queue if the passed url is not valid", async () => { const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData; @@ -503,7 +487,92 @@ describe("NotificationBackground", () => { expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); - it("adds a change password message to the queue if the user does not have an unlocked account", async () => { + it("only only includes ciphers in notification data matching a username if username was present in the modify form data", async () => { + const data: ModifyLoginCipherFormData = { + ...mockModifyLoginCipherFormData, + uri: "https://example.com", + username: "userName", + }; + + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + getAllDecryptedForUrlSpy.mockResolvedValueOnce([ + mock({ + id: "cipher-id-1", + login: { username: "test", password: "currentPassword" }, + }), + mock({ + id: "cipher-id-2", + login: { username: "username", password: "currentPassword" }, + }), + mock({ + id: "cipher-id-3", + login: { username: "uSeRnAmE", password: "currentPassword" }, + }), + ]); + + await notificationBackground.triggerChangedPasswordNotification(data, tab); + + expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( + ["cipher-id-2", "cipher-id-3"], + "example.com", + data?.newPassword, + sender.tab, + ); + }); + + it("adds a change password message to the queue with current password, if there is a current password, but no new password", async () => { + const data: ModifyLoginCipherFormData = { + ...mockModifyLoginCipherFormData, + uri: "https://example.com", + password: "newPasswordUpdatedElsewhere", + newPassword: null, + }; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + getAllDecryptedForUrlSpy.mockResolvedValueOnce([ + mock({ + id: "cipher-id-1", + login: { password: "currentPassword" }, + }), + ]); + await notificationBackground.triggerChangedPasswordNotification(data, tab); + + expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( + ["cipher-id-1"], + "example.com", + data?.password, + sender.tab, + ); + }); + + it("adds a change password message to the queue with new password, if new password is provided", async () => { + const data: ModifyLoginCipherFormData = { + ...mockModifyLoginCipherFormData, + uri: "https://example.com", + password: "password2", + newPassword: "password3", + }; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + getAllDecryptedForUrlSpy.mockResolvedValueOnce([ + mock({ + id: "cipher-id-1", + login: { password: "password1" }, + }), + mock({ + id: "cipher-id-4", + login: { password: "password4" }, + }), + ]); + await notificationBackground.triggerChangedPasswordNotification(data, tab); + + expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( + ["cipher-id-1", "cipher-id-4"], + "example.com", + data?.newPassword, + sender.tab, + ); + }); + + it("adds a change password message to the queue if the user has a locked account", async () => { const data: ModifyLoginCipherFormData = { ...mockModifyLoginCipherFormData, uri: "https://example.com", @@ -522,10 +591,12 @@ describe("NotificationBackground", () => { ); }); - it("skips adding a change password message to the queue if the multiple ciphers exist for the passed URL and the current password is not found within the list of ciphers", async () => { + it("doesn't add a password if there is no current or new password", async () => { const data: ModifyLoginCipherFormData = { ...mockModifyLoginCipherFormData, uri: "https://example.com", + password: null, + newPassword: null, }; activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ @@ -537,23 +608,6 @@ describe("NotificationBackground", () => { expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); - it("skips adding a change password message if more than one existing cipher is found with a matching password ", async () => { - const data: ModifyLoginCipherFormData = { - ...mockModifyLoginCipherFormData, - uri: "https://example.com", - }; - activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); - getAllDecryptedForUrlSpy.mockResolvedValueOnce([ - mock({ login: { username: "test", password: "password" } }), - mock({ login: { username: "test2", password: "password" } }), - ]); - - await notificationBackground.triggerChangedPasswordNotification(data, tab); - - expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); - expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); - }); - it("adds a change password message to the queue if a single cipher matches the passed current password", async () => { const data: ModifyLoginCipherFormData = { ...mockModifyLoginCipherFormData, @@ -570,28 +624,39 @@ describe("NotificationBackground", () => { await notificationBackground.triggerChangedPasswordNotification(data, tab); expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( - "cipher-id", + ["cipher-id"], "example.com", data?.newPassword, sender.tab, ); }); - it("skips adding a change password message if no current password is passed in the message and more than one cipher is found for a url", async () => { + it("adds a change password message with all matching ciphers if no current password is passed and more than one cipher is found for a url", async () => { const data: ModifyLoginCipherFormData = { ...mockModifyLoginCipherFormData, uri: "https://example.com", + password: null, }; activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ - mock({ login: { username: "test", password: "password" } }), - mock({ login: { username: "test2", password: "password" } }), + mock({ + id: "cipher-id-1", + login: { username: "test", password: "password" }, + }), + mock({ + id: "cipher-id-2", + login: { username: "test2", password: "password" }, + }), ]); await notificationBackground.triggerChangedPasswordNotification(data, tab); - expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); - expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); + expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( + ["cipher-id-1", "cipher-id-2"], + "example.com", + data?.newPassword, + sender.tab, + ); }); it("adds a change password message to the queue if no current password is passed with the message, but a single cipher is matched for the uri", async () => { @@ -611,7 +676,7 @@ describe("NotificationBackground", () => { await notificationBackground.triggerChangedPasswordNotification(data, tab); expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( - "cipher-id", + ["cipher-id"], "example.com", data?.newPassword, sender.tab, diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index d44bf2f1507..e27b50f13cd 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -213,14 +213,26 @@ export default class NotificationBackground { let cipherView: CipherView; if (cipherQueueMessage.type === NotificationType.ChangePassword) { const { - data: { cipherId }, + data: { cipherIds }, } = cipherQueueMessage; - cipherView = await this.getDecryptedCipherById(cipherId, activeUserId); + const cipherViews = await this.cipherService.getAllDecrypted(activeUserId); + return cipherViews + .filter((cipher) => cipherIds.includes(cipher.id)) + .map((cipherView) => { + const organizationType = getOrganizationType(cipherView.organizationId); + return this.convertToNotificationCipherData( + cipherView, + iconsServerUrl, + showFavicons, + organizationType, + ); + }); } else { cipherView = this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage); } const organizationType = getOrganizationType(cipherView.organizationId); + return [ this.convertToNotificationCipherData( cipherView, @@ -555,16 +567,6 @@ export default class NotificationBackground { return true; } - const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt(); - - if ( - changePasswordIsEnabled && - usernameMatches.length === 1 && - usernameMatches[0].login.password !== login.password - ) { - await this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, login.password, tab); - return true; - } return false; } @@ -603,45 +605,92 @@ export default class NotificationBackground { data: ModifyLoginCipherFormData, tab: chrome.tabs.Tab, ): Promise { - const changeData = { - url: data.uri, - currentPassword: data.password, - newPassword: data.newPassword, - }; - - const loginDomain = Utils.getDomain(changeData.url); - if (loginDomain == null) { + const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt(); + if (!changePasswordIsEnabled) { return false; } - - if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) { - await this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true); - return true; + const authStatus = await this.getAuthStatus(); + if (authStatus === AuthenticationStatus.LoggedOut) { + return false; } - - let id: string = null; const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(getOptionalUserId), ); - if (activeUserId == null) { + if (activeUserId === null) { + return false; + } + const loginDomain = Utils.getDomain(data.uri); + if (loginDomain === null) { return false; } - const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url, activeUserId); - if (changeData.currentPassword != null) { - const passwordMatches = ciphers.filter( - (c) => c.login.password === changeData.currentPassword, - ); - if (passwordMatches.length === 1) { - id = passwordMatches[0].id; - } - } else if (ciphers.length === 1) { - id = ciphers[0].id; - } - if (id != null) { - await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab); + const username: string | null = data.username || null; + const currentPassword = data.password || null; + const newPassword = data.newPassword || null; + + if (authStatus === AuthenticationStatus.Locked && newPassword !== null) { + await this.pushChangePasswordToQueue(null, loginDomain, newPassword, tab, true); return true; } + + let ciphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl( + data.uri, + activeUserId, + ); + + const normalizedUsername: string = username ? username.toLowerCase() : ""; + + const shouldMatchUsername = typeof username === "string" && username.length > 0; + + if (shouldMatchUsername) { + // Presence of a username should filter ciphers further. + ciphers = ciphers.filter( + (cipher) => + cipher.login.username !== null && + cipher.login.username.toLowerCase() === normalizedUsername, + ); + } + + if (ciphers.length === 1) { + const [cipher] = ciphers; + if ( + username !== null && + newPassword === null && + cipher.login.username === normalizedUsername && + cipher.login.password === currentPassword + ) { + // Assumed to be a login + return false; + } + } + + if (currentPassword && !newPassword) { + // Only use current password for change if no new password present. + if (ciphers.length > 0) { + await this.pushChangePasswordToQueue( + ciphers.map((cipher) => cipher.id), + loginDomain, + currentPassword, + tab, + ); + return true; + } + } + + if (newPassword) { + // Otherwise include all known ciphers. + if (ciphers.length > 0) { + await this.pushChangePasswordToQueue( + ciphers.map((cipher) => cipher.id), + loginDomain, + newPassword, + tab, + ); + + return true; + } + } + return false; } @@ -666,7 +715,7 @@ export default class NotificationBackground { } private async pushChangePasswordToQueue( - cipherId: string, + cipherIds: CipherView["id"][], loginDomain: string, newPassword: string, tab: chrome.tabs.Tab, @@ -677,7 +726,7 @@ export default class NotificationBackground { const launchTimestamp = new Date().getTime(); const message: AddChangePasswordNotificationQueueMessage = { type: NotificationType.ChangePassword, - data: { cipherId: cipherId, newPassword: newPassword }, + data: { cipherIds: cipherIds, newPassword: newPassword }, domain: loginDomain, tab: tab, launchTimestamp, @@ -716,12 +765,12 @@ export default class NotificationBackground { return; } - await this.saveOrUpdateCredentials(sender.tab, message.edit, message.folder); + await this.saveOrUpdateCredentials(sender.tab, message.cipherId, message.edit, message.folder); } async handleCipherUpdateRepromptResponse(message: NotificationBackgroundExtensionMessage) { if (message.success) { - await this.saveOrUpdateCredentials(message.tab, false, undefined, true); + await this.saveOrUpdateCredentials(message.tab, message.cipherId, false, undefined, true); } else { await BrowserApi.tabSendMessageData(message.tab, "saveCipherAttemptCompleted", { error: "Password reprompt failed", @@ -740,6 +789,7 @@ export default class NotificationBackground { */ private async saveOrUpdateCredentials( tab: chrome.tabs.Tab, + cipherId: CipherView["id"], edit: boolean, folderId?: string, skipReprompt: boolean = false, @@ -764,7 +814,7 @@ export default class NotificationBackground { if (queueMessage.type === NotificationType.ChangePassword) { const { - data: { cipherId, newPassword }, + data: { newPassword }, } = queueMessage; const cipherView = await this.getDecryptedCipherById(cipherId, activeUserId); @@ -1221,7 +1271,6 @@ export default class NotificationBackground { cipherView.folderId = folderId; cipherView.type = CipherType.Login; cipherView.login = loginView; - cipherView.organizationId = null; return cipherView; } diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts index 4657dfb6d1f..e08fe540710 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -455,12 +455,12 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg notificationType: NotificationType, ): boolean => { switch (notificationType) { - case NotificationTypes.Change: - return modifyLoginData?.newPassword && !modifyLoginData.username; case NotificationTypes.Add: return ( modifyLoginData?.username && !!(modifyLoginData.password || modifyLoginData.newPassword) ); + case NotificationTypes.Change: + return !!(modifyLoginData.password || modifyLoginData.newPassword); case NotificationTypes.AtRiskPassword: return !modifyLoginData.newPassword; case NotificationTypes.Unlock: diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts index 34ad5e1c9a9..7a392849996 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts @@ -4,8 +4,10 @@ import { BadgeButton } from "../../../content/components/buttons/badge-button"; import { EditButton } from "../../../content/components/buttons/edit-button"; import { NotificationTypes } from "../../../notification/abstractions/notification-bar"; import { I18n } from "../common-types"; +import { selectedCipher as selectedCipherSignal } from "../signals/selected-cipher"; export type CipherActionProps = { + cipherId: string; handleAction?: (e: Event) => void; i18n: I18n; itemName: string; @@ -15,6 +17,7 @@ export type CipherActionProps = { }; export function CipherAction({ + cipherId, handleAction = () => { /* no-op */ }, @@ -24,9 +27,17 @@ export function CipherAction({ theme, username, }: CipherActionProps) { + const selectCipherHandleAction = (e: Event) => { + selectedCipherSignal.set(cipherId); + try { + handleAction(e); + } finally { + selectedCipherSignal.set(null); + } + }; return notificationType === NotificationTypes.Change ? BadgeButton({ - buttonAction: handleAction, + buttonAction: selectCipherHandleAction, buttonText: i18n.notificationUpdate, itemName, theme, diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts index ab3b57f535c..3bfc44636b3 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts @@ -40,6 +40,7 @@ export function CipherItem({ if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) { cipherActionButton = html`
${CipherAction({ + cipherId: cipher.id, handleAction, i18n, itemName: name, diff --git a/apps/browser/src/autofill/content/components/signals/selected-cipher.ts b/apps/browser/src/autofill/content/components/signals/selected-cipher.ts new file mode 100644 index 00000000000..360457233f5 --- /dev/null +++ b/apps/browser/src/autofill/content/components/signals/selected-cipher.ts @@ -0,0 +1,3 @@ +import { signal } from "@lit-labs/signals"; + +export const selectedCipher = signal(null); diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 9ae6fcedc8f..fcf91ca2e91 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -1,6 +1,7 @@ import { render } from "lit"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { NotificationCipherData } from "../content/components/cipher/types"; @@ -8,6 +9,7 @@ import { CollectionView, I18n, OrgView } from "../content/components/common-type import { AtRiskNotification } from "../content/components/notification/at-risk-password/container"; import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container"; import { NotificationContainer } from "../content/components/notification/container"; +import { selectedCipher as selectedCipherSignal } from "../content/components/signals/selected-cipher"; import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder"; import { selectedVault as selectedVaultSignal } from "../content/components/signals/selected-vault"; @@ -180,9 +182,9 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { const i18n = getI18n(); const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); - const resolvedType = resolveNotificationType(notificationBarIframeInitData); - const headerMessage = getNotificationHeaderMessage(i18n, resolvedType); - const notificationTestId = getNotificationTestId(resolvedType); + const notificationType = resolveNotificationType(notificationBarIframeInitData); + const headerMessage = getNotificationHeaderMessage(i18n, notificationType); + const notificationTestId = getNotificationTestId(notificationType); appendHeaderMessageToTitle(headerMessage); document.body.innerHTML = ""; @@ -191,7 +193,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { const notificationConfig = { ...notificationBarIframeInitData, headerMessage, - type: resolvedType, + type: notificationType, notificationTestId, theme: resolvedTheme, personalVaultIsAllowed: !personalVaultDisallowed, @@ -201,7 +203,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { }; const handleSaveAction = () => { - sendSaveCipherMessage(true); + // cipher ID is null while vault is locked. + sendSaveCipherMessage(null, true); render( NotificationContainer({ @@ -262,7 +265,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { NotificationContainer({ ...notificationBarIframeInitData, headerMessage, - type: resolvedType, + type: notificationType, theme: resolvedTheme, notificationTestId, personalVaultIsAllowed: !personalVaultDisallowed, @@ -276,9 +279,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { }); function handleEditOrUpdateAction(e: Event) { - const notificationType = initData?.type; e.preventDefault(); - notificationType === "add" ? sendSaveCipherMessage(true) : sendSaveCipherMessage(false); + sendSaveCipherMessage(selectedCipherSignal.get(), notificationType === NotificationTypes.Add); } } @@ -291,6 +293,7 @@ function handleCloseNotification(e: Event) { } function handleSaveAction(e: Event) { + const selectedCipher = selectedCipherSignal.get(); const selectedVault = selectedVaultSignal.get(); const selectedFolder = selectedFolderSignal.get(); @@ -304,16 +307,16 @@ function handleSaveAction(e: Event) { } e.preventDefault(); - - sendSaveCipherMessage(removeIndividualVault(), selectedFolder); + sendSaveCipherMessage(selectedCipher, removeIndividualVault(), selectedFolder); if (removeIndividualVault()) { return; } } -function sendSaveCipherMessage(edit: boolean, folder?: string) { +function sendSaveCipherMessage(cipherId: CipherView["id"] | null, edit: boolean, folder?: string) { sendPlatformMessage({ command: "bgSaveCipher", + cipherId, folder, edit, }); diff --git a/apps/browser/src/autofill/services/autofill-badge-updater.service.ts b/apps/browser/src/autofill/services/autofill-badge-updater.service.ts index 06ddf16c8af..382c9efa7f8 100644 --- a/apps/browser/src/autofill/services/autofill-badge-updater.service.ts +++ b/apps/browser/src/autofill/services/autofill-badge-updater.service.ts @@ -1,4 +1,4 @@ -import { combineLatest, distinctUntilChanged, mergeMap, of, switchMap, withLatestFrom } from "rxjs"; +import { combineLatest, delay, distinctUntilChanged, mergeMap, of, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; @@ -10,7 +10,7 @@ import { Tab } from "../../platform/badge/badge-browser-api"; import { BadgeService } from "../../platform/badge/badge.service"; import { BadgeStatePriority } from "../../platform/badge/priority"; -const StateName = (tabId: number) => `autofill-badge-${tabId}`; +const StateName = "autofill-badge-updater"; export class AutofillBadgeUpdaterService { constructor( @@ -26,56 +26,30 @@ export class AutofillBadgeUpdaterService { switchMap((account) => (account?.id ? this.cipherService.ciphers$(account?.id) : of([]))), ); - // Recalculate badges for all active tabs when ciphers or active account changes - combineLatest({ - account: this.accountService.activeAccount$, - enableBadgeCounter: - this.badgeSettingsService.enableBadgeCounter$.pipe(distinctUntilChanged()), - ciphers: ciphers$, - }) - .pipe( + this.badgeService.setState(StateName, (tab) => { + return combineLatest({ + account: this.accountService.activeAccount$, + enableBadgeCounter: + this.badgeSettingsService.enableBadgeCounter$.pipe(distinctUntilChanged()), + ciphers: ciphers$.pipe(delay(100)), // Delay to allow cipherService.getAllDecryptedForUrl to pick up changes + }).pipe( mergeMap(async ({ account, enableBadgeCounter }) => { - if (!account) { - return; - } - - const tabs = await this.badgeService.getActiveTabs(); - - for (const tab of tabs) { - if (!tab.tabId) { - continue; - } - if (enableBadgeCounter) { - await this.setTabState(tab, account.id); - } else { - await this.clearTabState(tab.tabId); - } - } - }), - ) - .subscribe(); - - // Recalculate badge for a specific tab when it becomes active - this.badgeService.activeTabsUpdated$ - .pipe( - withLatestFrom( - this.accountService.activeAccount$, - this.badgeSettingsService.enableBadgeCounter$, - ), - mergeMap(async ([tabs, account, enableBadgeCounter]) => { if (!account || !enableBadgeCounter) { - return; + return undefined; } - for (const tab of tabs) { - await this.setTabState(tab, account.id); - } + return { + state: { + text: await this.calculateCountText(tab, account.id), + }, + priority: BadgeStatePriority.Default, + }; }), - ) - .subscribe(); + ); + }); } - private async setTabState(tab: Tab, userId: UserId) { + private async calculateCountText(tab: Tab, userId: UserId) { if (!tab.tabId) { this.logService.warning("Tab event received but tab id is undefined"); return; @@ -85,22 +59,9 @@ export class AutofillBadgeUpdaterService { const cipherCount = ciphers.length; if (cipherCount === 0) { - await this.clearTabState(tab.tabId); - return; + return undefined; } - const countText = cipherCount > 9 ? "9+" : cipherCount.toString(); - await this.badgeService.setState( - StateName(tab.tabId), - BadgeStatePriority.Default, - { - text: countText, - }, - tab.tabId, - ); - } - - private async clearTabState(tabId: number) { - await this.badgeService.clearState(StateName(tabId)); + return cipherCount > 9 ? "9+" : cipherCount.toString(); } } diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 0e238d14d23..73262962dbc 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -161,7 +161,7 @@ export default class AutofillService implements AutofillServiceInterface { // Create a timeout observable that emits an empty array if pageDetailsFromTab$ hasn't emitted within 1 second. const pageDetailsTimeout$ = timer(1000).pipe( - map(() => []), + map((): any => []), takeUntil(sharedPageDetailsFromTab$), ); @@ -2270,6 +2270,8 @@ export default class AutofillService implements AutofillServiceInterface { withoutForm: boolean, ): AutofillField | null { let usernameField: AutofillField = null; + let usernameFieldInSameForm: AutofillField = null; + for (let i = 0; i < pageDetails.fields.length; i++) { const f = pageDetails.fields[i]; if (AutofillService.forCustomFieldsOnly(f)) { @@ -2282,22 +2284,29 @@ export default class AutofillService implements AutofillServiceInterface { const includesUsernameFieldName = this.findMatchingFieldIndex(f, AutoFillConstants.UsernameFieldNames) > -1; + const isInSameForm = f.form === passwordField.form; if ( !f.disabled && (canBeReadOnly || !f.readonly) && - (withoutForm || f.form === passwordField.form || includesUsernameFieldName) && + (withoutForm || isInSameForm || includesUsernameFieldName) && (canBeHidden || f.viewable) && (f.type === "text" || f.type === "email" || f.type === "tel") ) { - usernameField = f; - // We found an exact match. No need to keep looking. - if (includesUsernameFieldName) { - break; + // Prioritize fields in the same form as the password field + if (isInSameForm) { + usernameFieldInSameForm = f; + if (includesUsernameFieldName) { + return f; + } + } else { + usernameField = f; } } } - return usernameField; + + // Prefer username field in same form, fall back to any username field + return usernameFieldInSameForm || usernameField; } /** diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 2ddee289044..47b1c9ea6df 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -433,7 +433,6 @@ export class CollectAutofillContentService implements CollectAutofillContentServ /** * Caches the autofill field element and its data. - * Will not cache the element if the index is less than 0. * * @param index - The index of the autofill field element * @param element - The autofill field element to cache @@ -444,10 +443,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ element: ElementWithOpId, autofillFieldData: AutofillField, ) { - if (index < 0) { - return; - } - + // Always cache the element, even if index is -1 (for dynamically added fields) this.autofillFieldElements.set(element, autofillFieldData); } @@ -1196,7 +1192,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ private setupOverlayListenersOnMutatedElements(mutatedElements: Node[]) { for (let elementIndex = 0; elementIndex < mutatedElements.length; elementIndex++) { const node = mutatedElements[elementIndex]; - const buildAutofillFieldItem = () => { + const buildAutofillFieldItem = async () => { if ( !this.isNodeFormFieldElement(node) || this.autofillFieldElements.get(node as ElementWithOpId) @@ -1206,7 +1202,17 @@ export class CollectAutofillContentService implements CollectAutofillContentServ // We are setting this item to a -1 index because we do not know its position in the DOM. // This value should be updated with the next call to collect page details. - void this.buildAutofillFieldItem(node as ElementWithOpId, -1); + const formFieldElement = node as ElementWithOpId; + const autofillField = await this.buildAutofillFieldItem(formFieldElement, -1); + + // Set up overlay listeners for the new field if we have the overlay service + if (autofillField && this.autofillOverlayContentService) { + this.setupOverlayOnField(formFieldElement, autofillField); + + if (this.domRecentlyMutated) { + this.updateAutofillElementsAfterMutation(); + } + } }; requestIdleCallbackPolyfill(buildAutofillFieldItem, { timeout: 1000 }); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 1ee3e57369b..f69693a1dbc 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1410,12 +1410,14 @@ export default class MainBackground { this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); PhishingDetectionService.initialize( - this.configService, + this.accountService, this.auditService, + this.billingAccountProfileStateService, + this.configService, + this.eventCollectionService, this.logService, this.storageService, this.taskSchedulerService, - this.eventCollectionService, ); this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService); @@ -1430,7 +1432,6 @@ export default class MainBackground { ); this.badgeService = new BadgeService( - this.stateProvider, new DefaultBadgeBrowserApi(this.platformUtilsService), this.logService, ); @@ -1920,7 +1921,6 @@ export default class MainBackground { this.badgeService, this.accountService, this.cipherService, - this.logService, this.taskService, ); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index d7aef0db375..9dc2bff65e5 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -145,6 +145,7 @@ export default class RuntimeBackground { if (totpCode != null) { this.platformUtilsService.copyToClipboard(totpCode); } + await this.main.updateOverlayCiphers(); break; } case ExtensionCommand.AutofillCard: { diff --git a/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.spec.ts b/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.spec.ts index 8d3c3ec5b31..d6aca6abea0 100644 --- a/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.spec.ts +++ b/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.spec.ts @@ -2,6 +2,8 @@ import { of } from "rxjs"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -10,35 +12,109 @@ import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling/task import { PhishingDetectionService } from "./phishing-detection.service"; describe("PhishingDetectionService", () => { + let accountService: AccountService; let auditService: AuditService; + let billingAccountProfileStateService: BillingAccountProfileStateService; + let configService: ConfigService; + let eventCollectionService: EventCollectionService; let logService: LogService; let storageService: AbstractStorageService; let taskSchedulerService: TaskSchedulerService; - let configService: ConfigService; - let eventCollectionService: EventCollectionService; beforeEach(() => { + accountService = { getAccount$: jest.fn(() => of(null)) } as any; auditService = { getKnownPhishingDomains: jest.fn() } as any; + billingAccountProfileStateService = {} as any; + configService = { getFeatureFlag$: jest.fn(() => of(false)) } as any; + eventCollectionService = {} as any; logService = { info: jest.fn(), debug: jest.fn(), warning: jest.fn(), error: jest.fn() } as any; storageService = { get: jest.fn(), save: jest.fn() } as any; taskSchedulerService = { registerTaskHandler: jest.fn(), setInterval: jest.fn() } as any; - configService = { getFeatureFlag$: jest.fn(() => of(false)) } as any; - eventCollectionService = {} as any; }); it("should initialize without errors", () => { expect(() => { PhishingDetectionService.initialize( - configService, + accountService, auditService, + billingAccountProfileStateService, + configService, + eventCollectionService, logService, storageService, taskSchedulerService, - eventCollectionService, ); }).not.toThrow(); }); + it("should enable phishing detection for premium account", (done) => { + const premiumAccount = { id: "user1" }; + accountService = { activeAccount$: of(premiumAccount) } as any; + configService = { getFeatureFlag$: jest.fn(() => of(true)) } as any; + billingAccountProfileStateService = { + hasPremiumFromAnySource$: jest.fn(() => of(true)), + } as any; + + // Patch _setup to call done + const setupSpy = jest + .spyOn(PhishingDetectionService as any, "_setup") + .mockImplementation(async () => { + expect(setupSpy).toHaveBeenCalled(); + done(); + }); + + // Run the initialization + PhishingDetectionService.initialize( + accountService, + auditService, + billingAccountProfileStateService, + configService, + eventCollectionService, + logService, + storageService, + taskSchedulerService, + ); + }); + + it("should not enable phishing detection for non-premium account", (done) => { + const nonPremiumAccount = { id: "user2" }; + accountService = { activeAccount$: of(nonPremiumAccount) } as any; + configService = { getFeatureFlag$: jest.fn(() => of(true)) } as any; + billingAccountProfileStateService = { + hasPremiumFromAnySource$: jest.fn(() => of(false)), + } as any; + + // Patch _setup to fail if called + // [FIXME] This test needs to check if the setupSpy fails or is called + // Refactor initialize in PhishingDetectionService to return a Promise or Observable that resolves/completes when initialization is done + // So that spy setups can be properly verified after initialization + // const setupSpy = jest + // .spyOn(PhishingDetectionService as any, "_setup") + // .mockImplementation(async () => { + // throw new Error("Should not call _setup"); + // }); + + // Patch _cleanup to call done + const cleanupSpy = jest + .spyOn(PhishingDetectionService as any, "_cleanup") + .mockImplementation(() => { + expect(cleanupSpy).toHaveBeenCalled(); + done(); + }); + + // Run the initialization + PhishingDetectionService.initialize( + accountService, + auditService, + billingAccountProfileStateService, + configService, + eventCollectionService, + logService, + storageService, + taskSchedulerService, + ); + }); + it("should detect phishing domains", () => { PhishingDetectionService["_knownPhishingDomains"].add("phishing.com"); const url = new URL("https://phishing.com"); diff --git a/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.ts b/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.ts index 1497ac96dba..54245ae17b4 100644 --- a/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.ts +++ b/apps/browser/src/dirt/phishing-detection/services/phishing-detection.service.ts @@ -1,7 +1,18 @@ -import { concatMap, delay, Subject, Subscription } from "rxjs"; +import { + combineLatest, + concatMap, + delay, + EMPTY, + map, + Subject, + Subscription, + switchMap, +} from "rxjs"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; 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"; @@ -41,31 +52,44 @@ export class PhishingDetectionService { private static _lastUpdateTime: number = 0; static initialize( - configService: ConfigService, + accountService: AccountService, auditService: AuditService, + billingAccountProfileStateService: BillingAccountProfileStateService, + configService: ConfigService, + eventCollectionService: EventCollectionService, logService: LogService, storageService: AbstractStorageService, taskSchedulerService: TaskSchedulerService, - eventCollectionService: EventCollectionService, ): void { this._auditService = auditService; this._logService = logService; this._storageService = storageService; this._taskSchedulerService = taskSchedulerService; - logService.info("[PhishingDetectionService] Initialize called"); + logService.info("[PhishingDetectionService] Initialize called. Checking prerequisites..."); - configService - .getFeatureFlag$(FeatureFlag.PhishingDetection) + combineLatest([ + accountService.activeAccount$, + configService.getFeatureFlag$(FeatureFlag.PhishingDetection), + ]) .pipe( - concatMap(async (enabled) => { - if (!enabled) { + switchMap(([account, featureEnabled]) => { + if (!account) { + logService.info("[PhishingDetectionService] No active account."); + this._cleanup(); + return EMPTY; + } + return billingAccountProfileStateService + .hasPremiumFromAnySource$(account.id) + .pipe(map((hasPremium) => ({ hasPremium, featureEnabled }))); + }), + concatMap(async ({ hasPremium, featureEnabled }) => { + if (!hasPremium || !featureEnabled) { logService.info( - "[PhishingDetectionService] Phishing detection feature flag is disabled.", + "[PhishingDetectionService] User does not have access to phishing detection service.", ); this._cleanup(); } else { - // Enable phishing detection service logService.info("[PhishingDetectionService] Enabling phishing detection service"); await this._setup(); } diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 2a45c846060..ee1656d5b68 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.9.0", + "version": "2025.10.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 6eeac8c8b39..dc773f91fd1 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.9.0", + "version": "2025.10.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/badge/badge-browser-api.ts b/apps/browser/src/platform/badge/badge-browser-api.ts index 79b50970400..80f84c3b46e 100644 --- a/apps/browser/src/platform/badge/badge-browser-api.ts +++ b/apps/browser/src/platform/badge/badge-browser-api.ts @@ -1,4 +1,16 @@ -import { concat, defer, filter, map, merge, Observable, shareReplay, switchMap } from "rxjs"; +import { + concat, + concatMap, + defer, + filter, + map, + merge, + Observable, + of, + pairwise, + shareReplay, + switchMap, +} from "rxjs"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -28,13 +40,37 @@ function tabFromChromeTab(tab: chrome.tabs.Tab): Tab { } export interface BadgeBrowserApi { - activeTabsUpdated$: Observable; + /** + * An observable that emits all currently active tabs whenever one or more active tabs change. + */ + activeTabs$: Observable; + /** + * An observable that emits tab events such as updates and activations. + */ + tabEvents$: Observable; + + /** + * Set the badge state for a specific tab. + * If the tabId is undefined the state will be applied to the browser action in general. + */ setState(state: RawBadgeState, tabId?: number): Promise; - getTabs(): Promise; - getActiveTabs(): Promise; } +export type TabEvent = + | { + type: "updated"; + tab: Tab; + } + | { + type: "activated"; + tab: Tab; + } + | { + type: "deactivated"; + tabId: number; + }; + export class DefaultBadgeBrowserApi implements BadgeBrowserApi { private badgeAction = BrowserApi.getBrowserAction(); private sidebarAction = BrowserApi.getSidebarAction(self); @@ -44,18 +80,25 @@ export class DefaultBadgeBrowserApi implements BadgeBrowserApi { shareReplay({ bufferSize: 1, refCount: true }), ); - activeTabsUpdated$ = concat( - defer(async () => await this.getActiveTabs()), + private createdOrUpdatedTabEvents$ = concat( + defer(async () => await this.getActiveTabs()).pipe( + switchMap((activeTabs) => { + const tabEvents: TabEvent[] = activeTabs.map((tab) => ({ + type: "activated", + tab, + })); + return of(...tabEvents); + }), + ), merge( this.onTabActivated$.pipe( - switchMap(async (activeInfo) => { - const tab = await BrowserApi.getTab(activeInfo.tabId); - - if (tab == undefined || tab.id == undefined || tab.url == undefined) { - return []; - } - - return [tabFromChromeTab(tab)]; + switchMap(async (activeInfo) => await BrowserApi.getTab(activeInfo.tabId)), + filter( + (tab): tab is chrome.tabs.Tab => + !(tab == undefined || tab.id == undefined || tab.url == undefined), + ), + switchMap(async (tab) => { + return { type: "activated", tab: tabFromChromeTab(tab) } satisfies TabEvent; }), ), fromChromeEvent(chrome.tabs.onUpdated).pipe( @@ -64,22 +107,58 @@ export class DefaultBadgeBrowserApi implements BadgeBrowserApi { // Only emit if the url was updated changeInfo.url != undefined, ), - map(([_tabId, _changeInfo, tab]) => [tabFromChromeTab(tab)]), + map( + ([_tabId, _changeInfo, tab]) => + ({ type: "updated", tab: tabFromChromeTab(tab) }) satisfies TabEvent, + ), ), fromChromeEvent(chrome.webNavigation.onCommitted).pipe( + filter(([details]) => details.transitionType === "reload"), map(([details]) => { - const toReturn: Tab[] = - details.transitionType === "reload" ? [{ tabId: details.tabId, url: details.url }] : []; - return toReturn; + return { + type: "updated", + tab: { tabId: details.tabId, url: details.url }, + } satisfies TabEvent; }), ), // NOTE: We're only sharing the active tab changes, not the full list of active tabs. // This is so that any new subscriber will get the latest active tabs immediately, but // doesn't re-subscribe to chrome events. ).pipe(shareReplay({ bufferSize: 1, refCount: true })), - ).pipe(filter((tabs) => tabs.length > 0)); + ); - async getActiveTabs(): Promise { + tabEvents$ = merge( + this.createdOrUpdatedTabEvents$, + this.createdOrUpdatedTabEvents$.pipe( + concatMap(async () => { + return this.getActiveTabs(); + }), + pairwise(), + map(([previousTabs, currentTabs]) => { + const previousTabIds = previousTabs.map((t) => t.tabId); + const currentTabIds = currentTabs.map((t) => t.tabId); + + const deactivatedTabIds = previousTabIds.filter((id) => !currentTabIds.includes(id)); + + return deactivatedTabIds.map( + (tabId) => + ({ + type: "deactivated", + tabId, + }) satisfies TabEvent, + ); + }), + switchMap((events) => of(...events)), + ), + ); + + activeTabs$ = this.tabEvents$.pipe( + concatMap(async () => { + return this.getActiveTabs(); + }), + ); + + private async getActiveTabs(): Promise { const tabs = await BrowserApi.getActiveTabs(); return tabs.filter((tab) => tab.id != undefined && tab.url != undefined).map(tabFromChromeTab); } @@ -96,10 +175,6 @@ export class DefaultBadgeBrowserApi implements BadgeBrowserApi { ]); } - async getTabs(): Promise { - return (await BrowserApi.tabsQuery({})).map((tab) => tab.id).filter((tab) => tab !== undefined); - } - private setIcon(icon: IconPaths, tabId?: number) { return Promise.all([this.setActionIcon(icon, tabId), this.setSidebarActionIcon(icon, tabId)]); } diff --git a/apps/browser/src/platform/badge/badge.service.spec.ts b/apps/browser/src/platform/badge/badge.service.spec.ts index d17e5dc0b5f..815941541e6 100644 --- a/apps/browser/src/platform/badge/badge.service.spec.ts +++ b/apps/browser/src/platform/badge/badge.service.spec.ts @@ -1,11 +1,10 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { Subscription } from "rxjs"; +import { EMPTY, Observable, of, Subscription } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { FakeAccountService, FakeStateProvider } from "@bitwarden/common/spec"; import { RawBadgeState } from "./badge-browser-api"; -import { BadgeService } from "./badge.service"; +import { BadgeService, BadgeStateFunction } from "./badge.service"; import { DefaultBadgeState } from "./consts"; import { BadgeIcon } from "./icon"; import { BadgeStatePriority } from "./priority"; @@ -14,7 +13,6 @@ import { MockBadgeBrowserApi } from "./test/mock-badge-browser-api"; describe("BadgeService", () => { let badgeApi: MockBadgeBrowserApi; - let stateProvider: FakeStateProvider; let logService!: MockProxy; let badgeService!: BadgeService; @@ -22,626 +20,834 @@ describe("BadgeService", () => { beforeEach(() => { badgeApi = new MockBadgeBrowserApi(); - stateProvider = new FakeStateProvider(new FakeAccountService({})); logService = mock(); - badgeService = new BadgeService(stateProvider, badgeApi, logService); + badgeService = new BadgeService(badgeApi, logService, 0); }); afterEach(() => { badgeServiceSubscription?.unsubscribe(); }); - describe("calling without tabId", () => { - const tabId = 1; - - describe("given a single tab is open", () => { - beforeEach(() => { - badgeApi.tabs = [tabId]; - badgeApi.setActiveTabs([tabId]); - badgeServiceSubscription = badgeService.startListening(); - }); - - it("sets provided state when no other state has been set", async () => { - const state: BadgeState = { - text: "text", - backgroundColor: "color", - icon: BadgeIcon.Locked, - }; - - await badgeService.setState("state-name", BadgeStatePriority.Default, state); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual(state); - }); - - it("sets default values when none are provided", async () => { - // This is a bit of a weird thing to do, but I don't think it's something we need to prohibit - const state: BadgeState = {}; - - await badgeService.setState("state-name", BadgeStatePriority.Default, state); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); - }); - - it("merges states when multiple same-priority states have been set", async () => { - await badgeService.setState("state-1", BadgeStatePriority.Default, { text: "text" }); - await badgeService.setState("state-2", BadgeStatePriority.Default, { - backgroundColor: "#fff", - }); - await badgeService.setState("state-3", BadgeStatePriority.Default, { - icon: BadgeIcon.Locked, - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - const expectedState: RawBadgeState = { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }; - expect(badgeApi.specificStates[tabId]).toEqual(expectedState); - }); - - it("overrides previous lower-priority state when higher-priority state is set", async () => { - await badgeService.setState("state-1", BadgeStatePriority.Low, { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }); - await badgeService.setState("state-2", BadgeStatePriority.Default, { - text: "override", - }); - await badgeService.setState("state-3", BadgeStatePriority.High, { - backgroundColor: "#aaa", - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - const expectedState: RawBadgeState = { - text: "override", - backgroundColor: "#aaa", - icon: BadgeIcon.Locked, - }; - expect(badgeApi.specificStates[tabId]).toEqual(expectedState); - }); - - it("removes override when a previously high-priority state is cleared", async () => { - await badgeService.setState("state-1", BadgeStatePriority.Low, { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }); - await badgeService.setState("state-2", BadgeStatePriority.Default, { - text: "override", - }); - await badgeService.clearState("state-2"); - - await new Promise((resolve) => setTimeout(resolve, 0)); - const expectedState: RawBadgeState = { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }; - expect(badgeApi.specificStates[tabId]).toEqual(expectedState); - }); - - it("sets default values when all states have been cleared", async () => { - await badgeService.setState("state-1", BadgeStatePriority.Low, { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }); - await badgeService.setState("state-2", BadgeStatePriority.Default, { - text: "override", - }); - await badgeService.setState("state-3", BadgeStatePriority.High, { - backgroundColor: "#aaa", - }); - await badgeService.clearState("state-1"); - await badgeService.clearState("state-2"); - await badgeService.clearState("state-3"); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); - }); - - it("sets default value high-priority state contains Unset", async () => { - await badgeService.setState("state-1", BadgeStatePriority.Low, { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }); - await badgeService.setState("state-3", BadgeStatePriority.High, { - icon: Unset, - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - const expectedState: RawBadgeState = { - text: "text", - backgroundColor: "#fff", - icon: DefaultBadgeState.icon, - }; - expect(badgeApi.specificStates[tabId]).toEqual(expectedState); - }); - - it("ignores medium-priority Unset when high-priority contains a value", async () => { - await badgeService.setState("state-1", BadgeStatePriority.Low, { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }); - await badgeService.setState("state-3", BadgeStatePriority.Default, { - icon: Unset, - }); - await badgeService.setState("state-3", BadgeStatePriority.High, { - icon: BadgeIcon.Unlocked, - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - const expectedState: RawBadgeState = { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Unlocked, - }; - expect(badgeApi.specificStates[tabId]).toEqual(expectedState); - }); - }); - - describe("given multiple tabs are open, only one active", () => { - const tabId = 1; - const tabIds = [1, 2, 3]; - - beforeEach(() => { - badgeApi.tabs = tabIds; - badgeApi.setActiveTabs([tabId]); - badgeServiceSubscription = badgeService.startListening(); - }); - - it("sets general state for active tab when no other state has been set", async () => { - const state: BadgeState = { - text: "text", - backgroundColor: "color", - icon: BadgeIcon.Locked, - }; - - await badgeService.setState("state-name", BadgeStatePriority.Default, state); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates).toEqual({ - 1: state, - 2: undefined, - 3: undefined, - }); - }); - - it("only updates the active tab when setting state", async () => { - const state: BadgeState = { - text: "text", - backgroundColor: "color", - icon: BadgeIcon.Locked, - }; - badgeApi.setState.mockReset(); - - await badgeService.setState("state-1", BadgeStatePriority.Default, state, tabId); - await badgeService.setState("state-2", BadgeStatePriority.Default, state, 2); - await badgeService.setState("state-2", BadgeStatePriority.Default, state, 2); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.setState).toHaveBeenCalledTimes(1); - }); - }); - - describe("given multiple tabs are open and multiple are active", () => { - const activeTabIds = [1, 2]; - const tabIds = [1, 2, 3]; - - beforeEach(() => { - badgeApi.tabs = tabIds; - badgeApi.setActiveTabs(activeTabIds); - badgeServiceSubscription = badgeService.startListening(); - }); - - it("sets general state for active tabs when no other state has been set", async () => { - const state: BadgeState = { - text: "text", - backgroundColor: "color", - icon: BadgeIcon.Locked, - }; - - await badgeService.setState("state-name", BadgeStatePriority.Default, state); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates).toEqual({ - 1: state, - 2: state, - 3: undefined, - }); - }); - - it("only updates the active tabs when setting general state", async () => { - const state: BadgeState = { - text: "text", - backgroundColor: "color", - icon: BadgeIcon.Locked, - }; - badgeApi.setState.mockReset(); - - await badgeService.setState("state-1", BadgeStatePriority.Default, state, 1); - await badgeService.setState("state-2", BadgeStatePriority.Default, state, 2); - await badgeService.setState("state-3", BadgeStatePriority.Default, state, 3); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.setState).toHaveBeenCalledTimes(2); - }); - }); - }); - - describe("calling with tabId", () => { - describe("given a single tab is open", () => { + describe("static state", () => { + describe("calling without tabId", () => { const tabId = 1; - beforeEach(() => { - badgeApi.tabs = [tabId]; - badgeApi.setActiveTabs([tabId]); - badgeServiceSubscription = badgeService.startListening(); - }); - - it("sets provided state when no other state has been set", async () => { - const state: BadgeState = { - text: "text", - backgroundColor: "color", - icon: BadgeIcon.Locked, - }; - - await badgeService.setState("state-name", BadgeStatePriority.Default, state, tabId); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual(state); - }); - - it("sets default values when none are provided", async () => { - // This is a bit of a weird thing to do, but I don't think it's something we need to prohibit - const state: BadgeState = {}; - - await badgeService.setState("state-name", BadgeStatePriority.Default, state, tabId); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); - }); - - it("merges tabId specific state with general states", async () => { - await badgeService.setState("general-state", BadgeStatePriority.Default, { text: "text" }); - await badgeService.setState( - "specific-state", - BadgeStatePriority.Default, - { - backgroundColor: "#fff", - }, - tabId, - ); - await badgeService.setState("general-state-2", BadgeStatePriority.Default, { - icon: BadgeIcon.Locked, + describe("given a single tab is open", () => { + beforeEach(() => { + badgeApi.tabs = [tabId]; + badgeApi.setActiveTabs([tabId]); + badgeServiceSubscription = badgeService.startListening(); }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual({ - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }); - }); - - it("merges states when multiple same-priority states with the same tabId have been set", async () => { - await badgeService.setState("state-1", BadgeStatePriority.Default, { text: "text" }, tabId); - await badgeService.setState( - "state-2", - BadgeStatePriority.Default, - { - backgroundColor: "#fff", - }, - tabId, - ); - await badgeService.setState( - "state-3", - BadgeStatePriority.Default, - { + it("sets provided state when no other state has been set", async () => { + const state: BadgeState = { + text: "text", + backgroundColor: "color", icon: BadgeIcon.Locked, - }, - tabId, - ); + }; - await new Promise((resolve) => setTimeout(resolve, 0)); - const expectedState: RawBadgeState = { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }; - expect(badgeApi.specificStates[tabId]).toEqual(expectedState); - }); + await badgeService.setState( + "state-name", + GeneralStateFunction(BadgeStatePriority.Default, state), + ); - it("overrides previous lower-priority state when higher-priority state with the same tabId is set", async () => { - await badgeService.setState( - "state-1", - BadgeStatePriority.Low, - { + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual(state); + }); + + it("sets default values when none are provided", async () => { + // This is a bit of a weird thing to do, but I don't think it's something we need to prohibit + const state: BadgeState = {}; + + await badgeService.setState( + "state-name", + GeneralStateFunction(BadgeStatePriority.Default, state), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); + }); + + it("sets default values even if state function never emits", async () => { + badgeService.setState("state-name", (_tab) => EMPTY); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); + }); + + it("merges states when multiple same-priority states have been set", async () => { + await badgeService.setState( + "state-1", + GeneralStateFunction(BadgeStatePriority.Default, { text: "text" }), + ); + await badgeService.setState( + "state-2", + GeneralStateFunction(BadgeStatePriority.Default, { + backgroundColor: "#fff", + }), + ); + await badgeService.setState( + "state-3", + GeneralStateFunction(BadgeStatePriority.Default, { + icon: BadgeIcon.Locked, + }), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + const expectedState: RawBadgeState = { text: "text", backgroundColor: "#fff", icon: BadgeIcon.Locked, - }, - tabId, - ); - await badgeService.setState( - "state-2", - BadgeStatePriority.Default, - { + }; + expect(badgeApi.specificStates[tabId]).toEqual(expectedState); + }); + + it("overrides previous lower-priority state when higher-priority state is set", async () => { + await badgeService.setState( + "state-1", + GeneralStateFunction(BadgeStatePriority.Low, { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }), + ); + await badgeService.setState( + "state-2", + GeneralStateFunction(BadgeStatePriority.Default, { + text: "override", + }), + ); + await badgeService.setState( + "state-3", + GeneralStateFunction(BadgeStatePriority.High, { + backgroundColor: "#aaa", + }), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + const expectedState: RawBadgeState = { text: "override", - }, - tabId, - ); - await badgeService.setState( - "state-3", - BadgeStatePriority.High, - { backgroundColor: "#aaa", - }, - tabId, - ); + icon: BadgeIcon.Locked, + }; + expect(badgeApi.specificStates[tabId]).toEqual(expectedState); + }); - await new Promise((resolve) => setTimeout(resolve, 0)); - const expectedState: RawBadgeState = { - text: "override", - backgroundColor: "#aaa", - icon: BadgeIcon.Locked, - }; - expect(badgeApi.specificStates[tabId]).toEqual(expectedState); - }); + it("removes override when a previously high-priority state is cleared", async () => { + await badgeService.setState( + "state-1", + GeneralStateFunction(BadgeStatePriority.Low, { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }), + ); + await badgeService.setState( + "state-2", + GeneralStateFunction(BadgeStatePriority.Default, { + text: "override", + }), + ); + await badgeService.clearState("state-2"); - it("overrides lower-priority tab-specific state when higher-priority general state is set", async () => { - await badgeService.setState( - "state-1", - BadgeStatePriority.Low, - { + await new Promise((resolve) => setTimeout(resolve, 0)); + const expectedState: RawBadgeState = { text: "text", backgroundColor: "#fff", icon: BadgeIcon.Locked, - }, - tabId, - ); - await badgeService.setState("state-2", BadgeStatePriority.Default, { - text: "override", - }); - await badgeService.setState("state-3", BadgeStatePriority.High, { - backgroundColor: "#aaa", + }; + expect(badgeApi.specificStates[tabId]).toEqual(expectedState); }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual({ - text: "override", - backgroundColor: "#aaa", - icon: BadgeIcon.Locked, - }); - }); + it("sets default values when all states have been cleared", async () => { + await badgeService.setState( + "state-1", + GeneralStateFunction(BadgeStatePriority.Low, { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }), + ); + await badgeService.setState( + "state-2", + GeneralStateFunction(BadgeStatePriority.Default, { + text: "override", + }), + ); + await badgeService.setState( + "state-3", + GeneralStateFunction(BadgeStatePriority.High, { + backgroundColor: "#aaa", + }), + ); + await badgeService.clearState("state-1"); + await badgeService.clearState("state-2"); + await badgeService.clearState("state-3"); - it("removes override when a previously high-priority state with the same tabId is cleared", async () => { - await badgeService.setState( - "state-1", - BadgeStatePriority.Low, - { + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); + }); + + it("sets default value high-priority state contains Unset", async () => { + await badgeService.setState( + "state-1", + GeneralStateFunction(BadgeStatePriority.Low, { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }), + ); + await badgeService.setState( + "state-3", + GeneralStateFunction(BadgeStatePriority.High, { + icon: Unset, + }), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + const expectedState: RawBadgeState = { text: "text", backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }, - tabId, - ); - await badgeService.setState( - "state-2", - BadgeStatePriority.Default, - { - text: "override", - }, - tabId, - ); - await badgeService.clearState("state-2"); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual({ - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, + icon: DefaultBadgeState.icon, + }; + expect(badgeApi.specificStates[tabId]).toEqual(expectedState); }); - }); - it("sets default state when all states with the same tabId have been cleared", async () => { - await badgeService.setState( - "state-1", - BadgeStatePriority.Low, - { + it("ignores medium-priority Unset when high-priority contains a value", async () => { + await badgeService.setState( + "state-1", + GeneralStateFunction(BadgeStatePriority.Low, { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }), + ); + await badgeService.setState( + "state-3", + GeneralStateFunction(BadgeStatePriority.Default, { + icon: Unset, + }), + ); + await badgeService.setState( + "state-3", + GeneralStateFunction(BadgeStatePriority.High, { + icon: BadgeIcon.Unlocked, + }), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + const expectedState: RawBadgeState = { text: "text", backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }, - tabId, - ); - await badgeService.setState( - "state-2", - BadgeStatePriority.Default, - { - text: "override", - }, - tabId, - ); - await badgeService.setState( - "state-3", - BadgeStatePriority.High, - { - backgroundColor: "#aaa", - }, - tabId, - ); - await badgeService.clearState("state-1"); - await badgeService.clearState("state-2"); - await badgeService.clearState("state-3"); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); - }); - - it("sets default value when high-priority state contains Unset", async () => { - await badgeService.setState( - "state-1", - BadgeStatePriority.Low, - { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }, - tabId, - ); - await badgeService.setState( - "state-3", - BadgeStatePriority.High, - { - icon: Unset, - }, - tabId, - ); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual({ - text: "text", - backgroundColor: "#fff", - icon: DefaultBadgeState.icon, - }); - }); - - it("ignores medium-priority Unset when high-priority contains a value", async () => { - await badgeService.setState( - "state-1", - BadgeStatePriority.Low, - { - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Locked, - }, - tabId, - ); - await badgeService.setState( - "state-3", - BadgeStatePriority.Default, - { - icon: Unset, - }, - tabId, - ); - await badgeService.setState( - "state-3", - BadgeStatePriority.High, - { icon: BadgeIcon.Unlocked, - }, - tabId, - ); + }; + expect(badgeApi.specificStates[tabId]).toEqual(expectedState); + }); + }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates[tabId]).toEqual({ - text: "text", - backgroundColor: "#fff", - icon: BadgeIcon.Unlocked, + describe("given multiple tabs are open, only one active", () => { + const tabId = 1; + const tabIds = [1, 2, 3]; + + beforeEach(() => { + badgeApi.tabs = tabIds; + badgeApi.setActiveTabs([tabId]); + badgeServiceSubscription = badgeService.startListening(); + }); + + it("sets general state for active tab when no other state has been set", async () => { + const state: BadgeState = { + text: "text", + backgroundColor: "color", + icon: BadgeIcon.Locked, + }; + + await badgeService.setState( + "state-name", + GeneralStateFunction(BadgeStatePriority.Default, state), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates).toEqual({ + 1: state, + 2: undefined, + 3: undefined, + }); + }); + + it("only updates the active tab when setting state", async () => { + const state: BadgeState = { + text: "text", + backgroundColor: "color", + icon: BadgeIcon.Locked, + }; + await badgeService.setState( + "state-1", + TabSpecificStateFunction(BadgeStatePriority.Default, state, tabId), + ); + await badgeService.setState( + "state-2", + TabSpecificStateFunction(BadgeStatePriority.Default, state, 2), + ); + await badgeService.setState( + "state-2", + TabSpecificStateFunction(BadgeStatePriority.Default, state, 2), + ); + await new Promise((resolve) => setTimeout(resolve, 0)); + + badgeApi.setState.mockReset(); + badgeApi.updateTab(tabId); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.setState).toHaveBeenCalledTimes(1); + }); + }); + + describe("given multiple tabs are open and multiple are active", () => { + const activeTabIds = [1, 2]; + const tabIds = [1, 2, 3]; + + beforeEach(() => { + badgeApi.tabs = tabIds; + badgeApi.setActiveTabs(activeTabIds); + badgeServiceSubscription = badgeService.startListening(); + }); + + it("sets general state for active tabs when no other state has been set", async () => { + const state: BadgeState = { + text: "text", + backgroundColor: "color", + icon: BadgeIcon.Locked, + }; + + await badgeService.setState( + "state-name", + GeneralStateFunction(BadgeStatePriority.Default, state), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates).toEqual({ + 1: state, + 2: state, + 3: undefined, + }); + }); + + it("only updates the active tabs when setting general state", async () => { + const state: BadgeState = { + text: "text", + backgroundColor: "color", + icon: BadgeIcon.Locked, + }; + + await badgeService.setState( + "state-1", + TabSpecificStateFunction(BadgeStatePriority.Default, state, 1), + ); + await badgeService.setState( + "state-2", + TabSpecificStateFunction(BadgeStatePriority.Default, state, 2), + ); + await badgeService.setState( + "state-3", + TabSpecificStateFunction(BadgeStatePriority.Default, state, 3), + ); + await new Promise((resolve) => setTimeout(resolve, 0)); + + badgeApi.setState.mockReset(); + badgeApi.updateTab(activeTabIds[0]); + badgeApi.updateTab(activeTabIds[1]); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.setState).toHaveBeenCalledTimes(2); }); }); }); - describe("given multiple tabs are open, only one active", () => { - const tabId = 1; - const tabIds = [1, 2, 3]; + describe("setting tab-specific states", () => { + describe("given a single tab is open", () => { + const tabId = 1; - beforeEach(() => { - badgeApi.tabs = tabIds; - badgeApi.setActiveTabs([tabId]); - badgeServiceSubscription = badgeService.startListening(); - }); - - it("sets tab-specific state for provided tab", async () => { - const generalState: BadgeState = { - text: "general-text", - backgroundColor: "general-color", - icon: BadgeIcon.Unlocked, - }; - const specificState: BadgeState = { - text: "tab-text", - icon: BadgeIcon.Locked, - }; - - await badgeService.setState("general-state", BadgeStatePriority.Default, generalState); - await badgeService.setState( - "tab-state", - BadgeStatePriority.Default, - specificState, - tabIds[0], - ); - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates).toEqual({ - [tabIds[0]]: { ...specificState, backgroundColor: "general-color" }, - [tabIds[1]]: undefined, - [tabIds[2]]: undefined, + beforeEach(() => { + badgeApi.tabs = [tabId]; + badgeApi.setActiveTabs([tabId]); + badgeServiceSubscription = badgeService.startListening(); }); - }); - }); - describe("given multiple tabs are open and multiple are active", () => { - const activeTabIds = [1, 2]; - const tabIds = [1, 2, 3]; + it("sets provided state when no other state has been set", async () => { + const state: BadgeState = { + text: "text", + backgroundColor: "color", + icon: BadgeIcon.Locked, + }; - beforeEach(() => { - badgeApi.tabs = tabIds; - badgeApi.setActiveTabs(activeTabIds); - badgeServiceSubscription = badgeService.startListening(); - }); + await badgeService.setState( + "state-name", + TabSpecificStateFunction(BadgeStatePriority.Default, state, tabId), + ); - it("sets general state for all active tabs when no other state has been set", async () => { - const generalState: BadgeState = { - text: "general-text", - backgroundColor: "general-color", - icon: BadgeIcon.Unlocked, - }; + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual(state); + }); - await badgeService.setState("general-state", BadgeStatePriority.Default, generalState); + it("sets default values when none are provided", async () => { + // This is a bit of a weird thing to do, but I don't think it's something we need to prohibit + const state: BadgeState = {}; - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates).toEqual({ - [tabIds[0]]: generalState, - [tabIds[1]]: generalState, - [tabIds[2]]: undefined, + await badgeService.setState( + "state-name", + TabSpecificStateFunction(BadgeStatePriority.Default, state, tabId), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); + }); + + it("merges tabId specific state with general states", async () => { + await badgeService.setState( + "general-state", + TabSpecificStateFunction( + BadgeStatePriority.Default, + { + text: "text", + }, + tabId, + ), + ); + await badgeService.setState( + "specific-state", + TabSpecificStateFunction( + BadgeStatePriority.Default, + { + backgroundColor: "#fff", + }, + tabId, + ), + ); + await badgeService.setState( + "general-state-2", + GeneralStateFunction(BadgeStatePriority.Default, { + icon: BadgeIcon.Locked, + }), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual({ + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }); + }); + + it("merges states when multiple same-priority states with the same tabId have been set", async () => { + await badgeService.setState( + "state-1", + TabSpecificStateFunction(BadgeStatePriority.Default, { text: "text" }, tabId), + ); + await badgeService.setState( + "state-2", + TabSpecificStateFunction( + BadgeStatePriority.Default, + { + backgroundColor: "#fff", + }, + tabId, + ), + ); + await badgeService.setState( + "state-3", + TabSpecificStateFunction( + BadgeStatePriority.Default, + { + icon: BadgeIcon.Locked, + }, + tabId, + ), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + const expectedState: RawBadgeState = { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }; + expect(badgeApi.specificStates[tabId]).toEqual(expectedState); + }); + + it("overrides previous lower-priority state when higher-priority state with the same tabId is set", async () => { + await badgeService.setState( + "state-1", + TabSpecificStateFunction( + BadgeStatePriority.Low, + { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }, + tabId, + ), + ); + await badgeService.setState( + "state-2", + TabSpecificStateFunction( + BadgeStatePriority.Default, + { + text: "override", + }, + tabId, + ), + ); + await badgeService.setState( + "state-3", + TabSpecificStateFunction( + BadgeStatePriority.High, + { + backgroundColor: "#aaa", + }, + tabId, + ), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + const expectedState: RawBadgeState = { + text: "override", + backgroundColor: "#aaa", + icon: BadgeIcon.Locked, + }; + expect(badgeApi.specificStates[tabId]).toEqual(expectedState); + }); + + it("overrides lower-priority tab-specific state when higher-priority general state is set", async () => { + await badgeService.setState( + "state-1", + TabSpecificStateFunction( + BadgeStatePriority.Low, + { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }, + tabId, + ), + ); + await badgeService.setState( + "state-2", + GeneralStateFunction(BadgeStatePriority.Default, { + text: "override", + }), + ); + await badgeService.setState( + "state-3", + GeneralStateFunction(BadgeStatePriority.High, { + backgroundColor: "#aaa", + }), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual({ + text: "override", + backgroundColor: "#aaa", + icon: BadgeIcon.Locked, + }); + }); + + it("removes override when a previously high-priority state with the same tabId is cleared", async () => { + await badgeService.setState( + "state-1", + TabSpecificStateFunction( + BadgeStatePriority.Low, + { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }, + tabId, + ), + ); + await badgeService.setState( + "state-2", + TabSpecificStateFunction( + BadgeStatePriority.Default, + { + text: "override", + }, + tabId, + ), + ); + await badgeService.clearState("state-2"); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual({ + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }); + }); + + it("sets default state when all states with the same tabId have been cleared", async () => { + await badgeService.setState( + "state-1", + TabSpecificStateFunction( + BadgeStatePriority.Low, + { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }, + tabId, + ), + ); + await badgeService.setState( + "state-2", + TabSpecificStateFunction( + BadgeStatePriority.Default, + { + text: "override", + }, + tabId, + ), + ); + await badgeService.setState( + "state-3", + TabSpecificStateFunction( + BadgeStatePriority.High, + { + backgroundColor: "#aaa", + }, + tabId, + ), + ); + await badgeService.clearState("state-1"); + await badgeService.clearState("state-2"); + await badgeService.clearState("state-3"); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); + }); + + it("sets default value when high-priority state contains Unset", async () => { + await badgeService.setState( + "state-1", + TabSpecificStateFunction( + BadgeStatePriority.Low, + { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }, + tabId, + ), + ); + await badgeService.setState( + "state-3", + TabSpecificStateFunction( + BadgeStatePriority.High, + { + icon: Unset, + }, + tabId, + ), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual({ + text: "text", + backgroundColor: "#fff", + icon: DefaultBadgeState.icon, + }); + }); + + it("ignores medium-priority Unset when high-priority contains a value", async () => { + await badgeService.setState( + "state-1", + TabSpecificStateFunction( + BadgeStatePriority.Low, + { + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Locked, + }, + tabId, + ), + ); + await badgeService.setState( + "state-3", + TabSpecificStateFunction( + BadgeStatePriority.Default, + { + icon: Unset, + }, + tabId, + ), + ); + await badgeService.setState( + "state-3", + TabSpecificStateFunction( + BadgeStatePriority.High, + { + icon: BadgeIcon.Unlocked, + }, + tabId, + ), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates[tabId]).toEqual({ + text: "text", + backgroundColor: "#fff", + icon: BadgeIcon.Unlocked, + }); }); }); - it("sets tab-specific state for provided tab", async () => { - const generalState: BadgeState = { - text: "general-text", - backgroundColor: "general-color", - icon: BadgeIcon.Unlocked, - }; - const specificState: BadgeState = { - text: "tab-text", - icon: BadgeIcon.Locked, - }; + describe("given multiple tabs are open, only one active", () => { + const tabId = 1; + const tabIds = [1, 2, 3]; - await badgeService.setState("general-state", BadgeStatePriority.Default, generalState); - await badgeService.setState( - "tab-state", - BadgeStatePriority.Default, - specificState, - tabIds[0], - ); + beforeEach(() => { + badgeApi.tabs = tabIds; + badgeApi.setActiveTabs([tabId]); + badgeServiceSubscription = badgeService.startListening(); + }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.specificStates).toEqual({ - [tabIds[0]]: { ...specificState, backgroundColor: "general-color" }, - [tabIds[1]]: generalState, - [tabIds[2]]: undefined, + it("sets tab-specific state for provided tab", async () => { + const generalState: BadgeState = { + text: "general-text", + backgroundColor: "general-color", + icon: BadgeIcon.Unlocked, + }; + const specificState: BadgeState = { + text: "tab-text", + icon: BadgeIcon.Locked, + }; + + await badgeService.setState( + "general-state", + GeneralStateFunction(BadgeStatePriority.Default, generalState), + ); + await badgeService.setState( + "tab-state", + TabSpecificStateFunction(BadgeStatePriority.Default, specificState, tabIds[0]), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates).toEqual({ + [tabIds[0]]: { ...specificState, backgroundColor: "general-color" }, + [tabIds[1]]: undefined, + [tabIds[2]]: undefined, + }); + }); + }); + + describe("given multiple tabs are open and multiple are active", () => { + const activeTabIds = [1, 2]; + const tabIds = [1, 2, 3]; + + beforeEach(() => { + badgeApi.tabs = tabIds; + badgeApi.setActiveTabs(activeTabIds); + badgeServiceSubscription = badgeService.startListening(); + }); + + it("sets general state for all active tabs when no other state has been set", async () => { + const generalState: BadgeState = { + text: "general-text", + backgroundColor: "general-color", + icon: BadgeIcon.Unlocked, + }; + + await badgeService.setState( + "general-state", + GeneralStateFunction(BadgeStatePriority.Default, generalState), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates).toEqual({ + [tabIds[0]]: generalState, + [tabIds[1]]: generalState, + [tabIds[2]]: undefined, + }); + }); + + it("sets tab-specific state for provided tab", async () => { + const generalState: BadgeState = { + text: "general-text", + backgroundColor: "general-color", + icon: BadgeIcon.Unlocked, + }; + const specificState: BadgeState = { + text: "tab-text", + icon: BadgeIcon.Locked, + }; + + await badgeService.setState( + "general-state", + GeneralStateFunction(BadgeStatePriority.Default, generalState), + ); + await badgeService.setState( + "tab-state", + TabSpecificStateFunction(BadgeStatePriority.Default, specificState, tabIds[0]), + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(badgeApi.specificStates).toEqual({ + [tabIds[0]]: { ...specificState, backgroundColor: "general-color" }, + [tabIds[1]]: generalState, + [tabIds[2]]: undefined, + }); + }); + + it("unsubscribes from state function when tab is deactivated", async () => { + let subscriptions = 0; + badgeService.setState("state", (tab) => { + return new Observable(() => { + subscriptions++; + return () => { + subscriptions--; + }; + }); + }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(subscriptions).toBe(activeTabIds.length); + + badgeApi.deactivateTab(activeTabIds[0]); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(subscriptions).toBe(activeTabIds.length - 1); }); }); }); }); }); + +/** + * Creates a dynamic state function that only provides a state for a specific tab. + */ +function TabSpecificStateFunction( + priority: BadgeStatePriority, + state: BadgeState, + tabId: number, +): BadgeStateFunction { + return (tab) => { + if (tab.tabId === tabId) { + return of({ + priority, + state, + }); + } + + return EMPTY; + }; +} + +/** + * Creates a dynamic state function that provides the same state for all tabs. + */ +function GeneralStateFunction(priority: BadgeStatePriority, state: BadgeState): BadgeStateFunction { + return (_tab) => + of({ + priority, + state, + }); +} diff --git a/apps/browser/src/platform/badge/badge.service.ts b/apps/browser/src/platform/badge/badge.service.ts index 5634aabec28..f6d799b2a80 100644 --- a/apps/browser/src/platform/badge/badge.service.ts +++ b/apps/browser/src/platform/badge/badge.service.ts @@ -1,69 +1,100 @@ -import { concatMap, filter, Subscription, withLatestFrom } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + combineLatestWith, + concatMap, + debounceTime, + filter, + groupBy, + map, + mergeMap, + Observable, + of, + startWith, + Subscription, + switchMap, + takeUntil, +} from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - BADGE_MEMORY, - GlobalState, - KeyDefinition, - StateProvider, -} from "@bitwarden/common/platform/state"; import { BadgeBrowserApi, RawBadgeState, Tab } from "./badge-browser-api"; import { DefaultBadgeState } from "./consts"; import { BadgeStatePriority } from "./priority"; import { BadgeState, Unset } from "./state"; -interface StateSetting { +const BADGE_UPDATE_DEBOUNCE_MS = 100; + +export interface BadgeStateSetting { priority: BadgeStatePriority; state: BadgeState; - tabId?: number; } -const BADGE_STATES = new KeyDefinition(BADGE_MEMORY, "badgeStates", { - deserializer: (value: Record) => value ?? {}, - cleanupDelayMs: 0, -}); +/** + * A function that returns the badge state for a specific tab. + * Return `undefined` to clear any previously set state for the tab. + */ +export type BadgeStateFunction = (tab: Tab) => Observable; export class BadgeService { - private serviceState: GlobalState>; - - /** - * An observable that emits whenever one or multiple tabs are updated and might need its state updated. - * Use this to know exactly which tabs to calculate the badge state for. - * This is not the same as `onActivated` which only emits when the active tab changes. - */ - activeTabsUpdated$ = this.badgeApi.activeTabsUpdated$; - - getActiveTabs(): Promise { - return this.badgeApi.getActiveTabs(); - } + private stateFunctions = new BehaviorSubject>({}); constructor( - private stateProvider: StateProvider, private badgeApi: BadgeBrowserApi, private logService: LogService, - ) { - this.serviceState = this.stateProvider.getGlobal(BADGE_STATES); - } + private debounceTimeMs: number = BADGE_UPDATE_DEBOUNCE_MS, + ) {} /** * Start listening for badge state changes. * Without this the service will not be able to update the badge state. */ startListening(): Subscription { - // React to tab changes - return this.badgeApi.activeTabsUpdated$ + // Default state function that always returns an empty state with lowest priority. + // This will ensure that there is always at least one state to consider when calculating the final badge state, + // so that the badge is cleared/set to default when no other states are set. + const defaultTabStateFunction: BadgeStateFunction = (_tab) => + of({ + priority: BadgeStatePriority.Low, + state: {}, + }); + + return this.badgeApi.tabEvents$ .pipe( - withLatestFrom(this.serviceState.state$), - filter(([activeTabs]) => activeTabs.length > 0), - concatMap(async ([activeTabs, serviceState]) => { - await Promise.all(activeTabs.map((tab) => this.updateBadge(serviceState, tab.tabId))); + groupBy((event) => (event.type === "deactivated" ? event.tabId : event.tab.tabId), { + duration: (group$) => + // Allow clean up of group when deactivated event arrives for this tabId + group$.pipe(filter((evt) => evt.type === "deactivated")), + }), + mergeMap((group$) => + group$.pipe( + // ignore deactivation events, only handle updates/activations + filter((evt) => evt.type !== "deactivated"), + map((evt) => evt.tab), + combineLatestWith(this.stateFunctions), + switchMap(([tab, dynamicStateFunctions]) => { + const functions = [...Object.values(dynamicStateFunctions), defaultTabStateFunction]; + + return combineLatest(functions.map((f) => f(tab).pipe(startWith(undefined)))).pipe( + map((states) => ({ + tab, + states: states.filter((s): s is BadgeStateSetting => s !== undefined), + })), + debounceTime(this.debounceTimeMs), + ); + }), + takeUntil(group$.pipe(filter((evt) => evt.type === "deactivated"))), + ), + ), + + concatMap(async (tabUpdate) => { + await this.updateBadge(tabUpdate.states, tabUpdate.tab.tabId); }), ) .subscribe({ error: (error: unknown) => { this.logService.error( - "Fatal error in badge service observable, badge will fail to update", + "BadgeService: Fatal error updating badge state. Badge will no longer be updated.", error, ); }, @@ -71,68 +102,45 @@ export class BadgeService { } /** - * Inform badge service of a new state that the badge should reflect. + * Register a function that takes an observable of active tab updates and returns an observable of state settings. + * This can be used to create dynamic badge states that react to tab changes. + * The returned observable should emit a new state setting whenever the badge state should be updated. * - * This will merge the new state with any existing states: + * This will merge all states: * - If the new state has a higher priority, it will override any lower priority states. * - If the new state has a lower priority, it will be ignored. * - If the name of the state is already in use, it will be updated. * - If the state has a `tabId` set, it will only apply to that tab. * - States with `tabId` can still be overridden by states without `tabId` if they have a higher priority. - * - * @param name The name of the state. This is used to identify the state and will be used to clear it later. - * @param priority The priority of the state (higher numbers are higher priority, but setting arbitrary numbers is not supported). - * @param state The state to set. - * @param tabId Limit this badge state to a specific tab. If this is not set, the state will be applied to all tabs. */ - async setState(name: string, priority: BadgeStatePriority, state: BadgeState, tabId?: number) { - const newServiceState = await this.serviceState.update((s) => ({ - ...s, - [name]: { priority, state, tabId }, - })); - await this.updateBadge(newServiceState, tabId); + setState(name: string, stateFunction: BadgeStateFunction) { + this.stateFunctions.next({ + ...this.stateFunctions.value, + [name]: stateFunction, + }); } /** - * Clear the state with the given name. + * Clear a state function previously registered with `setState`. * - * This will remove the state from the badge service and clear it from the badge. - * If the state is not found, nothing will happen. + * This will: + * - Stop the function from being called on future tab changes + * - Unsubscribe from any existing observables created by the function. + * - Clear any badge state previously set by the function. * - * @param name The name of the state to clear. + * @param name The name of the state function to clear. */ - async clearState(name: string) { - let clearedState: StateSetting | undefined; - - const newServiceState = await this.serviceState.update((s) => { - clearedState = s?.[name]; - - const newStates = { ...s }; - delete newStates[name]; - return newStates; - }); - - if (clearedState === undefined) { - return; - } - await this.updateBadge(newServiceState, clearedState.tabId); + clearState(name: string) { + const currentDynamicStateFunctions = this.stateFunctions.value; + const newDynamicStateFunctions = { ...currentDynamicStateFunctions }; + delete newDynamicStateFunctions[name]; + this.stateFunctions.next(newDynamicStateFunctions); } - private calculateState(states: Set, tabId?: number): RawBadgeState { - const sortedStates = [...states].sort((a, b) => a.priority - b.priority); + private calculateState(states: BadgeStateSetting[]): RawBadgeState { + const sortedStates = states.sort((a, b) => a.priority - b.priority); - let filteredStates = sortedStates; - if (tabId !== undefined) { - // Filter out states that are not applicable to the current tab. - // If a state has no tabId, it is considered applicable to all tabs. - // If a state has a tabId, it is only applicable to that tab. - filteredStates = sortedStates.filter((s) => s.tabId === tabId || s.tabId === undefined); - } else { - // If no tabId is provided, we only want states that are not tab-specific. - filteredStates = sortedStates.filter((s) => s.tabId === undefined); - } - - const mergedState = filteredStates + const mergedState = sortedStates .map((s) => s.state) .reduce>((acc: Partial, state: BadgeState) => { const newState = { ...acc }; @@ -156,43 +164,16 @@ export class BadgeService { * This will only update the badge if the active tab is the same as the tabId of the latest change. * If the active tab is not set, it will not update the badge. * - * @param activeTab The currently active tab. * @param serviceState The current state of the badge service. If this is null or undefined, an empty set will be assumed. * @param tabId Tab id for which the the latest state change applied to. Set this to activeTab.tabId to force an update. + * @param activeTabs The currently active tabs. If not provided, it will be fetched from the badge API. */ - private async updateBadge( - serviceState: Record | null | undefined, - tabId: number | undefined, - ) { - const activeTabs = await this.badgeApi.getActiveTabs(); - if (tabId !== undefined && !activeTabs.some((tab) => tab.tabId === tabId)) { - return; // No need to update the badge if the state is not for the active tab. - } - - const tabIdsToUpdate = tabId ? [tabId] : activeTabs.map((tab) => tab.tabId); - - for (const tabId of tabIdsToUpdate) { - if (tabId === undefined) { - continue; // Skip if tab id is undefined. - } - - const newBadgeState = this.calculateState(new Set(Object.values(serviceState ?? {})), tabId); - try { - await this.badgeApi.setState(newBadgeState, tabId); - } catch (error) { - this.logService.error("Failed to set badge state", error); - } - } - - if (tabId === undefined) { - // If no tabId was provided we should also update the general badge state - const newBadgeState = this.calculateState(new Set(Object.values(serviceState ?? {}))); - - try { - await this.badgeApi.setState(newBadgeState, tabId); - } catch (error) { - this.logService.error("Failed to set general badge state", error); - } + private async updateBadge(serviceState: BadgeStateSetting[], tabId: number) { + const newBadgeState = this.calculateState(serviceState); + try { + await this.badgeApi.setState(newBadgeState, tabId); + } catch (error) { + this.logService.error("Failed to set badge state", error); } } } diff --git a/apps/browser/src/platform/badge/scope.ts b/apps/browser/src/platform/badge/scope.ts new file mode 100644 index 00000000000..5d6cb8dd4e7 --- /dev/null +++ b/apps/browser/src/platform/badge/scope.ts @@ -0,0 +1,23 @@ +export const BadgeStateScope = { + /** + * The state is global and applies to all users. + */ + Global: { type: "global" } satisfies BadgeStateScope, + /** + * The state is for a specific user and only applies to that user when they are unlocked. + */ + UserUnlocked: (userId: string) => + ({ + type: "user_unlocked", + userId, + }) satisfies BadgeStateScope, +} as const; + +export type BadgeStateScope = + | { + type: "global"; + } + | { + type: "user_unlocked"; + userId: string; + }; diff --git a/apps/browser/src/platform/badge/state.ts b/apps/browser/src/platform/badge/state.ts index 0731ad81f41..ea6b52b28e4 100644 --- a/apps/browser/src/platform/badge/state.ts +++ b/apps/browser/src/platform/badge/state.ts @@ -1,6 +1,8 @@ import { BadgeIcon } from "./icon"; -export const Unset = Symbol("Unset badge state"); +const UnsetValue = Symbol("Unset badge state"); + +export const Unset = UnsetValue as typeof UnsetValue; export type Unset = typeof Unset; export type BadgeState = { diff --git a/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts b/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts index a1b79c29cb8..9f8db3f23ef 100644 --- a/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts +++ b/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts @@ -1,33 +1,50 @@ -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, concat, defer, of, Subject, switchMap } from "rxjs"; -import { BadgeBrowserApi, RawBadgeState, Tab } from "../badge-browser-api"; +import { BadgeBrowserApi, RawBadgeState, Tab, TabEvent } from "../badge-browser-api"; export class MockBadgeBrowserApi implements BadgeBrowserApi { - private _activeTabsUpdated$ = new BehaviorSubject([]); - activeTabsUpdated$ = this._activeTabsUpdated$.asObservable(); + private _activeTabs$ = new BehaviorSubject([]); + private _tabEvents$ = new Subject(); + activeTabs$ = this._activeTabs$.asObservable(); specificStates: Record = {}; generalState?: RawBadgeState; tabs: number[] = []; - activeTabs: number[] = []; - getActiveTabs(): Promise { - return Promise.resolve( - this.activeTabs.map( - (tabId) => - ({ - tabId, - url: `https://example.com/${tabId}`, - }) satisfies Tab, - ), - ); + tabEvents$ = concat( + defer(() => [this.activeTabs]).pipe( + switchMap((activeTabs) => { + const tabEvents: TabEvent[] = activeTabs.map((tab) => ({ + type: "activated", + tab, + })); + return of(...tabEvents); + }), + ), + this._tabEvents$.asObservable(), + ); + + get activeTabs() { + return this._activeTabs$.value; } setActiveTabs(tabs: number[]) { - this.activeTabs = tabs; - this._activeTabsUpdated$.next( - tabs.map((tabId) => ({ tabId, url: `https://example.com/${tabId}` })), - ); + this._activeTabs$.next(tabs.map((tabId) => ({ tabId, url: `https://example.com/${tabId}` }))); + + tabs.forEach((tabId) => { + this._tabEvents$.next({ + type: "activated", + tab: { tabId, url: `https://example.com/${tabId}` }, + }); + }); + } + + updateTab(tabId: number) { + this._tabEvents$.next({ type: "updated", tab: { tabId, url: `https://example.com/${tabId}` } }); + } + + deactivateTab(tabId: number) { + this._tabEvents$.next({ type: "deactivated", tabId }); } setState = jest.fn().mockImplementation((state: RawBadgeState, tabId?: number): Promise => { @@ -39,8 +56,4 @@ export class MockBadgeBrowserApi implements BadgeBrowserApi { return Promise.resolve(); }); - - getTabs(): Promise { - return Promise.resolve(this.tabs); - } } diff --git a/apps/browser/src/platform/popup/services/browser-router.service.ts b/apps/browser/src/platform/popup/services/browser-router.service.ts index 2d449b8a0f2..e1de1fdd29d 100644 --- a/apps/browser/src/platform/popup/services/browser-router.service.ts +++ b/apps/browser/src/platform/popup/services/browser-router.service.ts @@ -21,9 +21,7 @@ export class BrowserRouterService { child = child.firstChild; } - // TODO: Eslint upgrade. Please resolve this since the ?? does nothing - // eslint-disable-next-line no-constant-binary-expression - const updateUrl = !child?.data?.doNotSaveUrl ?? true; + const updateUrl = !child?.data?.doNotSaveUrl; if (updateUrl) { this.setPreviousUrl(event.url); diff --git a/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts index b545618c0ce..2e9746642f4 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts @@ -62,9 +62,7 @@ export class PopupRouterCacheService { child = child.firstChild; } - // TODO: Eslint upgrade. Please resolve this since the ?? does nothing - // eslint-disable-next-line no-constant-binary-expression - return !child?.data?.doNotSaveUrl ?? true; + return !child?.data?.doNotSaveUrl; }), switchMap((event) => this.push(event.url)), ) diff --git a/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts b/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts index b835c711853..7dcd8a12392 100644 --- a/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts +++ b/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts @@ -70,8 +70,8 @@ export class BrowserSystemNotificationService implements SystemNotificationsServ } async clear(clearInfo: SystemNotificationClearInfo): Promise { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - chrome.notifications.clear(clearInfo.id); + await chrome.notifications.clear(clearInfo.id); + return undefined; } isSupported(): boolean { diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index b69d7b73672..17a812f451c 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -12,6 +12,7 @@ import { tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; +import { LoginViaWebAuthnComponent } from "@bitwarden/angular/auth/login-via-webauthn/login-via-webauthn.component"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; import { @@ -22,6 +23,7 @@ import { UserLockIcon, VaultIcon, LockIcon, + TwoFactorAuthSecurityKeyIcon, DeactivatedOrg, } from "@bitwarden/assets/svg"; import { @@ -403,6 +405,29 @@ const routes: Routes = [ }, ], }, + { + path: "login-with-passkey", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + pageIcon: TwoFactorAuthSecurityKeyIcon, + pageTitle: { + key: "logInWithPasskey", + }, + pageSubtitle: { + key: "readingPasskeyLoadingInfo", + }, + elevation: 1, + showBackButton: true, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaWebAuthnComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, { path: "sso", canActivate: [unauthGuardFn(unauthRouteOverrides)], diff --git a/apps/browser/src/popup/scss/pages.scss b/apps/browser/src/popup/scss/pages.scss index 4c2daab2159..56c5f80c86c 100644 --- a/apps/browser/src/popup/scss/pages.scss +++ b/apps/browser/src/popup/scss/pages.scss @@ -137,3 +137,8 @@ body.body-full { margin-bottom: 0; } } + +/** Temporary fix for avatar, will not be required once we migrate to tailwind preflight **/ +bit-avatar svg { + display: block; +} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index d87c9417c85..7a10dc2343f 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -29,6 +29,7 @@ import { TwoFactorAuthDuoComponentService, TwoFactorAuthWebAuthnComponentService, SsoComponentService, + NewDeviceVerificationComponentService, } from "@bitwarden/auth/angular"; import { LockService, @@ -36,6 +37,7 @@ import { SsoUrlService, LogoutService, } from "@bitwarden/auth/common"; +import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -119,10 +121,12 @@ import { SystemNotificationsService } from "@bitwarden/common/platform/system-no import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; +import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { @@ -145,8 +149,6 @@ import { DefaultSshImportPromptService, PasswordRepromptService, SshImportPromptService, - CipherArchiveService, - DefaultCipherArchiveService, } from "@bitwarden/vault"; import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service"; @@ -708,14 +710,12 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: CipherArchiveService, useClass: DefaultCipherArchiveService, - deps: [ - CipherService, - ApiService, - DialogService, - PasswordRepromptService, - BillingAccountProfileStateService, - ConfigService, - ], + deps: [CipherService, ApiService, BillingAccountProfileStateService, ConfigService], + }), + safeProvider({ + provide: NewDeviceVerificationComponentService, + useClass: ExtensionNewDeviceVerificationComponentService, + deps: [], }), ]; diff --git a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html index 16d9b6a322a..6c2bc3f77a0 100644 --- a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html +++ b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html @@ -1,5 +1,4 @@ - {{ (taskCount === 1 ? "reviewAndChangeAtRiskPassword" : "reviewAndChangeAtRiskPasswordsPlural") 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 16cc04ce612..7ee7e141ee5 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 @@ -40,7 +40,7 @@ @if (canArchive$ | async) { } 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 6979f519f2d..324ea2ffcdf 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 @@ -13,6 +13,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; @@ -28,7 +29,7 @@ import { MenuModule, ToastService, } from "@bitwarden/components"; -import { CipherArchiveService, PasswordRepromptService } from "@bitwarden/vault"; +import { PasswordRepromptService } from "@bitwarden/vault"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; 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 f271b255c3e..718043b4e85 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 @@ -204,6 +204,7 @@ describe("VaultPopupAutofillService", () => { describe("doAutofill()", () => { it("should return true if autofill is successful", async () => { + mockCipher.id = "test-cipher-id"; mockAutofillService.doAutoFill.mockResolvedValue(null); const result = await service.doAutofill(mockCipher); expect(result).toBe(true); @@ -251,6 +252,7 @@ describe("VaultPopupAutofillService", () => { }); it("should copy TOTP code to clipboard if available", async () => { + mockCipher.id = "test-cipher-id-with-totp"; const totpCode = "123456"; mockAutofillService.doAutoFill.mockResolvedValue(totpCode); await service.doAutofill(mockCipher); @@ -405,5 +407,26 @@ describe("VaultPopupAutofillService", () => { }); }); }); + describe("handleAutofillSuggestionUsed", () => { + const cipherId = "cipher-123"; + + beforeEach(() => { + mockCipherService.updateLastUsedDate.mockResolvedValue(undefined); + }); + + it("updates last used date when there is an active user", async () => { + await service.handleAutofillSuggestionUsed({ cipherId }); + + expect(mockCipherService.updateLastUsedDate).toHaveBeenCalledTimes(1); + expect(mockCipherService.updateLastUsedDate).toHaveBeenCalledWith(cipherId, mockUserId); + }); + + it("does nothing when there is no active user", async () => { + accountService.activeAccount$ = of(null); + await service.handleAutofillSuggestionUsed({ cipherId }); + + expect(mockCipherService.updateLastUsedDate).not.toHaveBeenCalled(); + }); + }); }); }); 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 2d30e857573..3d5b35cded6 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 @@ -16,6 +16,7 @@ import { } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { isUrlInList } from "@bitwarden/common/autofill/utils"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -268,6 +269,7 @@ export class VaultPopupAutofillService { }); return false; } + await this.handleAutofillSuggestionUsed({ cipherId: cipher.id }); return true; } @@ -326,6 +328,21 @@ export class VaultPopupAutofillService { return didAutofill; } + /** + * When a user autofills with an autofill suggestion outside of the inline menu, + * update the cipher's last used date. + * + * @param message - The message containing the cipher ID that was used + */ + async handleAutofillSuggestionUsed(message: { cipherId: string }) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (activeUserId) { + await this.cipherService.updateLastUsedDate(message.cipherId, activeUserId); + } + } + /** * Attempts to autofill the given cipher and, upon successful autofill, saves the URI to the cipher. * Will copy any TOTP code to the clipboard if available after successful autofill. 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 6499719b64f..513e159f7aa 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 @@ -14,6 +14,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { mockAccountServiceWith, ObservableTracker } from "@bitwarden/common/spec"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; @@ -26,7 +27,6 @@ import { RestrictedItemTypesService, } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; -import { CipherArchiveService } from "@bitwarden/vault"; import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; 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 3e4b793737e..fa56b45c080 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 @@ -26,6 +26,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; @@ -35,7 +36,6 @@ import { CipherViewLike, CipherViewLikeUtils, } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; -import { CipherArchiveService } from "@bitwarden/vault"; import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator"; import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service"; 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 eecd1f2fd68..692e21d0084 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 @@ -761,11 +761,13 @@ function createSeededVaultPopupListFiltersService( const collectionServiceMock = { decryptedCollections$: () => seededCollections$, getAllNested: () => - seededCollections$.value.map((c) => ({ - children: [], - node: c, - parent: null, - })), + seededCollections$.value.map( + (c): TreeNode => ({ + children: [], + node: c, + parent: null as any, + }), + ), } as any; const folderServiceMock = { 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 05d0ea8d444..08db7d5d4ab 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 @@ -14,7 +14,11 @@ import { take, } from "rxjs"; -import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { + CollectionService, + CollectionTypes, + CollectionView, +} from "@bitwarden/admin-console/common"; import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model"; import { sortDefaultCollections } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service"; @@ -473,7 +477,14 @@ export class VaultPopupListFiltersService { }); }), map((tree) => - tree.nestedList.map((c) => this.convertToChipSelectOption(c, "bwi-collection-shared")), + tree.nestedList.map((c) => + this.convertToChipSelectOption( + c, + c.node.type === CollectionTypes.DefaultUserCollection + ? "bwi-user" + : "bwi-collection-shared", + ), + ), ), shareReplay({ bufferSize: 1, refCount: true }), ); diff --git a/apps/browser/src/vault/popup/settings/archive.component.ts b/apps/browser/src/vault/popup/settings/archive.component.ts index c3e078a9274..d685beb0287 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.ts +++ b/apps/browser/src/vault/popup/settings/archive.component.ts @@ -9,6 +9,7 @@ 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 { CipherId, UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -22,7 +23,11 @@ import { ToastService, TypographyModule, } from "@bitwarden/components"; -import { CanDeleteCipherDirective, CipherArchiveService } from "@bitwarden/vault"; +import { + CanDeleteCipherDirective, + DecryptionFailureDialogComponent, + PasswordRepromptService, +} from "@bitwarden/vault"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -56,6 +61,7 @@ export class ArchiveComponent { private toastService = inject(ToastService); private i18nService = inject(I18nService); private cipherArchiveService = inject(CipherArchiveService); + private passwordRepromptService = inject(PasswordRepromptService); private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); @@ -69,7 +75,7 @@ export class ArchiveComponent { ); async view(cipher: CipherView) { - if (!(await this.cipherArchiveService.canInteract(cipher))) { + if (!(await this.canInteract(cipher))) { return; } @@ -79,7 +85,7 @@ export class ArchiveComponent { } async edit(cipher: CipherView) { - if (!(await this.cipherArchiveService.canInteract(cipher))) { + if (!(await this.canInteract(cipher))) { return; } @@ -89,7 +95,7 @@ export class ArchiveComponent { } async delete(cipher: CipherView) { - if (!(await this.cipherArchiveService.canInteract(cipher))) { + if (!(await this.canInteract(cipher))) { return; } const confirmed = await this.dialogService.openSimpleDialog({ @@ -118,7 +124,7 @@ export class ArchiveComponent { } async unarchive(cipher: CipherView) { - if (!(await this.cipherArchiveService.canInteract(cipher))) { + if (!(await this.canInteract(cipher))) { return; } const activeUserId = await firstValueFrom(this.userId$); @@ -132,7 +138,7 @@ export class ArchiveComponent { } async clone(cipher: CipherView) { - if (!(await this.cipherArchiveService.canInteract(cipher))) { + if (!(await this.canInteract(cipher))) { return; } @@ -156,4 +162,21 @@ export class ArchiveComponent { }, }); } + + /** + * Check if the user is able to interact with the cipher + * (password re-prompt / decryption failure checks). + * @param cipher + * @private + */ + private canInteract(cipher: CipherView) { + if (cipher.decryptionFailure) { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: [cipher.id as CipherId], + }); + return false; + } + + return this.passwordRepromptService.passwordRepromptCheck(cipher); + } } diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html index 630c55d0038..3c1278b4d44 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html @@ -37,7 +37,7 @@ @if (userCanArchive() || showArchiveFilter()) { - {{ "archive" | i18n }} + {{ "archiveNoun" | i18n }} diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts index 4e8a49b2591..92cbf951ead 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts @@ -9,9 +9,9 @@ import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; 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 { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components"; -import { CipherArchiveService } from "@bitwarden/vault"; import { BrowserApi } from "../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; diff --git a/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.spec.ts b/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.spec.ts index f2567ef4267..b84d17a8375 100644 --- a/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.spec.ts +++ b/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.spec.ts @@ -1,12 +1,11 @@ -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { SecurityTask, TaskService } from "@bitwarden/common/vault/tasks"; -import { LogService } from "@bitwarden/logging"; -import { UserId } from "@bitwarden/user-core"; +import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; -import { BadgeService } from "../../platform/badge/badge.service"; +import { Tab } from "../../platform/badge/badge-browser-api"; +import { BadgeService, BadgeStateFunction } from "../../platform/badge/badge.service"; import { BadgeIcon } from "../../platform/badge/icon"; import { BadgeStatePriority } from "../../platform/badge/priority"; import { Unset } from "../../platform/badge/state"; @@ -18,34 +17,32 @@ describe("AtRiskCipherBadgeUpdaterService", () => { let service: AtRiskCipherBadgeUpdaterService; let setState: jest.Mock; - let clearState: jest.Mock; - let warning: jest.Mock; let getAllDecryptedForUrl: jest.Mock; let getTab: jest.Mock; let addListener: jest.Mock; - const activeAccount$ = new BehaviorSubject({ id: "test-account-id" }); - const cipherViews$ = new BehaviorSubject([]); - const pendingTasks$ = new BehaviorSubject([]); - const userId = "test-user-id" as UserId; + let activeAccount$: BehaviorSubject<{ id: string }>; + let cipherViews$: BehaviorSubject>; + let pendingTasks$: BehaviorSubject; beforeEach(async () => { setState = jest.fn().mockResolvedValue(undefined); - clearState = jest.fn().mockResolvedValue(undefined); - warning = jest.fn(); getAllDecryptedForUrl = jest.fn().mockResolvedValue([]); getTab = jest.fn(); addListener = jest.fn(); + activeAccount$ = new BehaviorSubject({ id: "test-account-id" }); + cipherViews$ = new BehaviorSubject>([]); + pendingTasks$ = new BehaviorSubject([]); + jest.spyOn(BrowserApi, "addListener").mockImplementation(addListener); jest.spyOn(BrowserApi, "getTab").mockImplementation(getTab); service = new AtRiskCipherBadgeUpdaterService( - { setState, clearState } as unknown as BadgeService, + { setState } as unknown as BadgeService, { activeAccount$ } as unknown as AccountService, - { cipherViews$, getAllDecryptedForUrl } as unknown as CipherService, - { warning } as unknown as LogService, - { pendingTasks$ } as unknown as TaskService, + { cipherViews$: () => cipherViews$, getAllDecryptedForUrl } as unknown as CipherService, + { pendingTasks$: () => pendingTasks$ } as unknown as TaskService, ); await service.init(); @@ -55,30 +52,41 @@ describe("AtRiskCipherBadgeUpdaterService", () => { jest.restoreAllMocks(); }); + it("registers dynamic state function on init", () => { + expect(setState).toHaveBeenCalledWith("at-risk-cipher-badge", expect.any(Function)); + }); + it("clears the tab state when there are no ciphers and no pending tasks", async () => { - const tab = { id: 1 } as chrome.tabs.Tab; + const tab: Tab = { tabId: 1, url: "https://bitwarden.com" }; + const stateFunction = setState.mock.calls[0][1]; - await service["setTabState"](tab, userId, []); + const state = await firstValueFrom(stateFunction(tab)); - expect(clearState).toHaveBeenCalledWith("at-risk-cipher-badge-1"); + expect(state).toBeUndefined(); }); it("sets state when there are pending tasks for the tab", async () => { - const tab = { id: 3, url: "https://bitwarden.com" } as chrome.tabs.Tab; - const pendingTasks: SecurityTask[] = [{ id: "task1", cipherId: "cipher1" } as SecurityTask]; + const tab: Tab = { tabId: 3, url: "https://bitwarden.com" }; + const stateFunction: BadgeStateFunction = setState.mock.calls[0][1]; + const pendingTasks: SecurityTask[] = [ + { + id: "task1", + cipherId: "cipher1", + type: SecurityTaskType.UpdateAtRiskCredential, + } as SecurityTask, + ]; + pendingTasks$.next(pendingTasks); getAllDecryptedForUrl.mockResolvedValueOnce([{ id: "cipher1" }]); - await service["setTabState"](tab, userId, pendingTasks); + const state = await firstValueFrom(stateFunction(tab)); - expect(setState).toHaveBeenCalledWith( - "at-risk-cipher-badge-3", - BadgeStatePriority.High, - { + expect(state).toEqual({ + priority: BadgeStatePriority.High, + state: { icon: BadgeIcon.Berry, text: Unset, backgroundColor: Unset, }, - 3, - ); + }); }); }); diff --git a/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.ts b/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.ts index 47364958ad8..a06c208ebe2 100644 --- a/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.ts +++ b/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.ts @@ -1,26 +1,18 @@ -import { combineLatest, map, mergeMap, of, Subject, switchMap } from "rxjs"; +import { combineLatest, concatMap, map, of, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; +import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { BadgeService } from "../../platform/badge/badge.service"; import { BadgeIcon } from "../../platform/badge/icon"; import { BadgeStatePriority } from "../../platform/badge/priority"; import { Unset } from "../../platform/badge/state"; -import { BrowserApi } from "../../platform/browser/browser-api"; -const StateName = (tabId: number) => `at-risk-cipher-badge-${tabId}`; +const StateName = "at-risk-cipher-badge"; export class AtRiskCipherBadgeUpdaterService { - private tabReplaced$ = new Subject<{ addedTab: chrome.tabs.Tab; removedTabId: number }>(); - private tabUpdated$ = new Subject(); - private tabRemoved$ = new Subject(); - private tabActivated$ = new Subject(); - private activeUserData$ = this.accountService.activeAccount$.pipe( filterOutNullish(), switchMap((user) => @@ -40,124 +32,36 @@ export class AtRiskCipherBadgeUpdaterService { private badgeService: BadgeService, private accountService: AccountService, private cipherService: CipherService, - private logService: LogService, private taskService: TaskService, - ) { - combineLatest({ - replaced: this.tabReplaced$, - activeUserData: this.activeUserData$, - }) - .pipe( - mergeMap(async ({ replaced, activeUserData: [userId, pendingTasks] }) => { - await this.clearTabState(replaced.removedTabId); - await this.setTabState(replaced.addedTab, userId, pendingTasks); - }), - ) - .subscribe(() => {}); - - combineLatest({ - tab: this.tabActivated$, - activeUserData: this.activeUserData$, - }) - .pipe( - mergeMap(async ({ tab, activeUserData: [userId, pendingTasks] }) => { - await this.setTabState(tab, userId, pendingTasks); - }), - ) - .subscribe(); - - combineLatest({ - tab: this.tabUpdated$, - activeUserData: this.activeUserData$, - }) - .pipe( - mergeMap(async ({ tab, activeUserData: [userId, pendingTasks] }) => { - await this.setTabState(tab, userId, pendingTasks); - }), - ) - .subscribe(); - - this.tabRemoved$ - .pipe( - mergeMap(async (tabId) => { - await this.clearTabState(tabId); - }), - ) - .subscribe(); - } + ) {} init() { - BrowserApi.addListener(chrome.tabs.onReplaced, async (addedTabId, removedTabId) => { - const newTab = await BrowserApi.getTab(addedTabId); - if (!newTab) { - this.logService.warning( - `Tab replaced event received but new tab not found (id: ${addedTabId})`, - ); - return; - } + this.badgeService.setState(StateName, (tab) => { + return this.activeUserData$.pipe( + concatMap(async ([userId, pendingTasks]) => { + const ciphers = tab.url + ? await this.cipherService.getAllDecryptedForUrl(tab.url, userId, [], undefined, true) + : []; - this.tabReplaced$.next({ - removedTabId, - addedTab: newTab, - }); + const hasPendingTasksForTab = pendingTasks.some((task) => + ciphers.some((cipher) => cipher.id === task.cipherId && !cipher.isDeleted), + ); + + if (!hasPendingTasksForTab) { + return undefined; + } + + return { + priority: BadgeStatePriority.High, + state: { + icon: BadgeIcon.Berry, + // Unset text and background color to use default badge appearance + text: Unset, + backgroundColor: Unset, + }, + }; + }), + ); }); - - BrowserApi.addListener(chrome.tabs.onUpdated, (_, changeInfo, tab) => { - if (changeInfo.url) { - this.tabUpdated$.next(tab); - } - }); - - BrowserApi.addListener(chrome.tabs.onActivated, async (activeInfo) => { - const tab = await BrowserApi.getTab(activeInfo.tabId); - if (!tab) { - this.logService.warning( - `Tab activated event received but tab not found (id: ${activeInfo.tabId})`, - ); - return; - } - - this.tabActivated$.next(tab); - }); - - BrowserApi.addListener(chrome.tabs.onRemoved, (tabId, _) => this.tabRemoved$.next(tabId)); - } - - /** Sets the pending task state for the tab */ - private async setTabState(tab: chrome.tabs.Tab, userId: UserId, pendingTasks: SecurityTask[]) { - if (!tab.id) { - this.logService.warning("Tab event received but tab id is undefined"); - return; - } - - const ciphers = tab.url - ? await this.cipherService.getAllDecryptedForUrl(tab.url, userId, [], undefined, true) - : []; - - const hasPendingTasksForTab = pendingTasks.some((task) => - ciphers.some((cipher) => cipher.id === task.cipherId && !cipher.isDeleted), - ); - - if (!hasPendingTasksForTab) { - await this.clearTabState(tab.id); - return; - } - - await this.badgeService.setState( - StateName(tab.id), - BadgeStatePriority.High, - { - icon: BadgeIcon.Berry, - // Unset text and background color to use default badge appearance - text: Unset, - backgroundColor: Unset, - }, - tab.id, - ); - } - - /** Clears the pending task state from a tab */ - private async clearTabState(tabId: number) { - await this.badgeService.clearState(StateName(tabId)); } } diff --git a/apps/browser/store/locales/hu/copy.resx b/apps/browser/store/locales/hu/copy.resx index 814ebabaada..e3a5c733eb5 100644 --- a/apps/browser/store/locales/hu/copy.resx +++ b/apps/browser/store/locales/hu/copy.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden Jelszókezelő - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Legyen otthon, munkában, vagy úton, a Bitwarden könnyen biztosítja jelszavát, kulcsait, és kényes információit. Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Legyen otthon, munkában, vagy úton, a Bitwarden könnyen biztosítja jelszavát, kulcsait, és kényes információit. A széf szinkronizálása és elérése több eszközön. diff --git a/apps/cli/CLAUDE.md b/apps/cli/CLAUDE.md new file mode 100644 index 00000000000..72ec31eaaf5 --- /dev/null +++ b/apps/cli/CLAUDE.md @@ -0,0 +1,13 @@ +# CLI - Critical Rules + +- **ALWAYS** output structured JSON when `process.env.BW_RESPONSE === "true"` + - Use Response objects (MessageResponse, ListResponse, etc.) from `/apps/cli/src/models/response/` + - DON'T write free-form text that breaks JSON parsing + +- **NEVER** use `console.log()` for output + - Use `CliUtils.writeLn()` to respect `BW_QUIET` and `BW_RESPONSE` environment variables + - Use the `ConsoleLogService` from the `ServiceContainer` + +- **ALWAYS** respect `BW_CLEANEXIT` environment variable + - Exit code 0 even on errors when `BW_CLEANEXIT` is set + - Required for scripting environments that need clean exits diff --git a/apps/cli/package.json b/apps/cli/package.json index 659a68d13a5..02db5317a26 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.9.0", + "version": "2025.10.0", "keywords": [ "bitwarden", "password", diff --git a/apps/cli/project.json b/apps/cli/project.json new file mode 100644 index 00000000000..229738818a7 --- /dev/null +++ b/apps/cli/project.json @@ -0,0 +1,86 @@ +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "name": "cli", + "projectType": "application", + "sourceRoot": "apps/cli/src", + "tags": ["scope:cli", "type:app"], + "targets": { + "build": { + "executor": "@nx/webpack:webpack", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "oss-dev", + "options": { + "outputPath": "dist/apps/cli", + "webpackConfig": "apps/cli/webpack.config.js", + "tsConfig": "apps/cli/tsconfig.json", + "main": "apps/cli/src/bw.ts", + "target": "node", + "compiler": "tsc" + }, + "configurations": { + "oss": { + "mode": "production", + "outputPath": "dist/apps/cli/oss" + }, + "oss-dev": { + "mode": "development", + "outputPath": "dist/apps/cli/oss-dev" + }, + "commercial": { + "mode": "production", + "outputPath": "dist/apps/cli/commercial", + "webpackConfig": "bitwarden_license/bit-cli/webpack.config.js", + "main": "bitwarden_license/bit-cli/src/bw.ts", + "tsConfig": "bitwarden_license/bit-cli/tsconfig.json" + }, + "commercial-dev": { + "mode": "development", + "outputPath": "dist/apps/cli/commercial-dev", + "webpackConfig": "bitwarden_license/bit-cli/webpack.config.js", + "main": "bitwarden_license/bit-cli/src/bw.ts", + "tsConfig": "bitwarden_license/bit-cli/tsconfig.json" + } + } + }, + "serve": { + "executor": "@nx/webpack:webpack", + "defaultConfiguration": "oss-dev", + "options": { + "outputPath": "dist/apps/cli", + "webpackConfig": "apps/cli/webpack.config.js", + "tsConfig": "apps/cli/tsconfig.json", + "main": "apps/cli/src/bw.ts", + "target": "node", + "compiler": "tsc", + "watch": true + }, + "configurations": { + "oss-dev": { + "mode": "development", + "outputPath": "dist/apps/cli/oss-dev" + }, + "commercial-dev": { + "mode": "development", + "outputPath": "dist/apps/cli/commercial-dev", + "webpackConfig": "bitwarden_license/bit-cli/webpack.config.js", + "main": "bitwarden_license/bit-cli/src/bw.ts", + "tsConfig": "bitwarden_license/bit-cli/tsconfig.json" + } + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "apps/cli/jest.config.js" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/cli/**/*.ts"] + } + } + } +} diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 133c9658ae7..7e5058dcff0 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -34,6 +34,7 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -369,6 +370,15 @@ export class LoginCommand { return await this.handleSuccessResponse(response); } catch (e) { + if ( + e instanceof ErrorResponse && + e.message === "Username or password is incorrect. Try again." + ) { + const env = await firstValueFrom(this.environmentService.environment$); + const host = Utils.getHost(env.getWebVaultUrl()); + return Response.error(this.i18nService.t("invalidMasterPasswordConfirmEmailAndHost", host)); + } + return Response.error(e); } } diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 92674aa3dcd..f4216196ead 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import * as inquirer from "inquirer"; import { firstValueFrom, map, switchMap } from "rxjs"; import { UpdateCollectionRequest } from "@bitwarden/admin-console/common"; @@ -9,6 +10,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; 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 { 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"; @@ -40,6 +42,7 @@ export class EditCommand { private accountService: AccountService, private cliRestrictedItemTypesService: CliRestrictedItemTypesService, private policyService: PolicyService, + private billingAccountProfileStateService: BillingAccountProfileStateService, ) {} async run( @@ -92,6 +95,10 @@ export class EditCommand { private async editCipher(id: string, req: CipherExport) { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipher = await this.cipherService.get(id, activeUserId); + const hasPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), + ); + if (cipher == null) { return Response.notFound(); } @@ -102,6 +109,17 @@ export class EditCommand { } cipherView = CipherExport.toView(req, cipherView); + // When a user is editing an archived cipher and does not have premium, automatically unarchive it + if (cipherView.isArchived && !hasPremium) { + const acceptedPrompt = await this.promptForArchiveEdit(); + + if (!acceptedPrompt) { + return Response.error("Edit cancelled."); + } + + cipherView.archivedDate = null; + } + const isCipherRestricted = await this.cliRestrictedItemTypesService.isCipherRestricted(cipherView); if (isCipherRestricted) { @@ -240,6 +258,38 @@ export class EditCommand { return Response.error(e); } } + + /** Prompt the user to accept movement of their cipher back to the their vault. */ + private async promptForArchiveEdit(): Promise { + // When running in serve or no interaction mode, automatically accept the prompt + if (process.env.BW_SERVE === "true" || process.env.BW_NOINTERACTION === "true") { + CliUtils.writeLn( + "Archive is only available with a Premium subscription, which has ended. Your edit was saved and the item was moved back to your vault.", + ); + return true; + } + + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "list", + name: "confirm", + message: + "When you edit and save details for an archived item without a Premium subscription, it'll be moved from your archive back to your vault.", + choices: [ + { + name: "Move now", + value: "confirmed", + }, + { + name: "Cancel", + value: "cancel", + }, + ], + }); + + return answer.confirm === "confirmed"; + } } class Options { diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index d8b4cfcfd10..e5174f67913 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -16,6 +16,7 @@ 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"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; @@ -45,6 +46,7 @@ export class ListCommand { private accountService: AccountService, private keyService: KeyService, private cliRestrictedItemTypesService: CliRestrictedItemTypesService, + private cipherArchiveService: CipherArchiveService, ) {} async run(object: string, cmdOptions: Record): Promise { @@ -71,8 +73,13 @@ export class ListCommand { let ciphers: CipherView[]; const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const userCanArchive = await firstValueFrom( + this.cipherArchiveService.userCanArchive$(activeUserId), + ); options.trash = options.trash || false; + options.archived = userCanArchive && options.archived; + if (options.url != null && options.url.trim() !== "") { ciphers = await this.cipherService.getAllDecryptedForUrl(options.url, activeUserId); } else { @@ -85,9 +92,12 @@ export class ListCommand { options.organizationId != null ) { ciphers = ciphers.filter((c) => { - if (options.trash !== c.isDeleted) { + const matchesStateOptions = this.matchesStateOptions(c, options); + + if (!matchesStateOptions) { return false; } + if (options.folderId != null) { if (options.folderId === "notnull" && c.folderId != null) { return true; @@ -131,11 +141,16 @@ export class ListCommand { return false; }); } else if (options.search == null || options.search.trim() === "") { - ciphers = ciphers.filter((c) => options.trash === c.isDeleted); + ciphers = ciphers.filter((c) => this.matchesStateOptions(c, options)); } if (options.search != null && options.search.trim() !== "") { - ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash); + ciphers = this.searchService.searchCiphersBasic( + ciphers, + options.search, + options.trash, + options.archived, + ); } ciphers = await this.cliRestrictedItemTypesService.filterRestrictedCiphers(ciphers); @@ -287,6 +302,17 @@ export class ListCommand { const res = new ListResponse(organizations.map((o) => new OrganizationResponse(o))); return Response.success(res); } + + /** + * Checks if the cipher passes the state filter options. + * @returns true if the cipher matches the requested state + */ + private matchesStateOptions(c: CipherView, options: Options): boolean { + const passesTrashFilter = options.trash === c.isDeleted; + const passesArchivedFilter = options.archived === c.isArchived; + + return passesTrashFilter && passesArchivedFilter; + } } class Options { @@ -296,6 +322,7 @@ class Options { search: string; url: string; trash: boolean; + archived: boolean; constructor(passedOptions: Record) { this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId; @@ -304,5 +331,6 @@ class Options { this.search = passedOptions?.search; this.url = passedOptions?.url; this.trash = CliUtils.convertBooleanOption(passedOptions?.trash); + this.archived = CliUtils.convertBooleanOption(passedOptions?.archived); } } diff --git a/apps/cli/src/commands/restore.command.ts b/apps/cli/src/commands/restore.command.ts index 0b30193ffd4..d8cefdfce5d 100644 --- a/apps/cli/src/commands/restore.command.ts +++ b/apps/cli/src/commands/restore.command.ts @@ -2,8 +2,14 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { CipherId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { UserId } from "@bitwarden/user-core"; import { Response } from "../models/response"; @@ -12,6 +18,8 @@ export class RestoreCommand { private cipherService: CipherService, private accountService: AccountService, private cipherAuthorizationService: CipherAuthorizationService, + private cipherArchiveService: CipherArchiveService, + private configService: ConfigService, ) {} async run(object: string, id: string): Promise { @@ -30,10 +38,23 @@ export class RestoreCommand { private async restoreCipher(id: string) { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipher = await this.cipherService.get(id, activeUserId); + const isArchivedVaultEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive), + ); if (cipher == null) { return Response.notFound(); } + + if (cipher.archivedDate && isArchivedVaultEnabled) { + return this.restoreArchivedCipher(cipher, activeUserId); + } else { + return this.restoreDeletedCipher(cipher, activeUserId); + } + } + + /** Restores a cipher from the trash. */ + private async restoreDeletedCipher(cipher: Cipher, userId: UserId) { if (cipher.deletedDate == null) { return Response.badRequest("Cipher is not in trash."); } @@ -47,7 +68,17 @@ export class RestoreCommand { } try { - await this.cipherService.restoreWithServer(id, activeUserId); + await this.cipherService.restoreWithServer(cipher.id, userId); + return Response.success(); + } catch (e) { + return Response.error(e); + } + } + + /** Restore a cipher from the archive vault */ + private async restoreArchivedCipher(cipher: Cipher, userId: UserId) { + try { + await this.cipherArchiveService.unarchiveWithServer(cipher.id as CipherId, userId); return Response.success(); } catch (e) { return Response.error(e); diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index c0ec37d3c9c..5bf19333f35 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -51,7 +51,7 @@ export class ServeCommand { .use(koaBodyParser()) .use(koaJson({ pretty: false, param: "pretty" })); - this.serveConfigurator.configureRouter(router); + await this.serveConfigurator.configureRouter(router); server.use(router.routes()).use(router.allowedMethods()); diff --git a/apps/cli/src/locales/en/messages.json b/apps/cli/src/locales/en/messages.json index 4a8c774ea42..18079bd2409 100644 --- a/apps/cli/src/locales/en/messages.json +++ b/apps/cli/src/locales/en/messages.json @@ -41,6 +41,15 @@ "invalidMasterPassword": { "message": "Invalid master password." }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "sessionTimeout": { "message": "Your session has timed out. Please go back and try logging in again." }, diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index 6ae2776eae7..3c80d12af2f 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -5,6 +5,8 @@ import * as koaRouter from "@koa/router"; import * as koa from "koa"; import { firstValueFrom, map } from "rxjs"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + import { ConfirmCommand } from "./admin-console/commands/confirm.command"; import { ShareCommand } from "./admin-console/commands/share.command"; import { LockCommand } from "./auth/commands/lock.command"; @@ -26,6 +28,7 @@ import { SendListCommand, SendRemovePasswordCommand, } from "./tools/send"; +import { ArchiveCommand } from "./vault/archive.command"; import { CreateCommand } from "./vault/create.command"; import { DeleteCommand } from "./vault/delete.command"; import { SyncCommand } from "./vault/sync.command"; @@ -40,6 +43,7 @@ export class OssServeConfigurator { private statusCommand: StatusCommand; private syncCommand: SyncCommand; private deleteCommand: DeleteCommand; + private archiveCommand: ArchiveCommand; private confirmCommand: ConfirmCommand; private restoreCommand: RestoreCommand; private lockCommand: LockCommand; @@ -81,6 +85,7 @@ export class OssServeConfigurator { this.serviceContainer.accountService, this.serviceContainer.keyService, this.serviceContainer.cliRestrictedItemTypesService, + this.serviceContainer.cipherArchiveService, ); this.createCommand = new CreateCommand( this.serviceContainer.cipherService, @@ -104,6 +109,7 @@ export class OssServeConfigurator { this.serviceContainer.accountService, this.serviceContainer.cliRestrictedItemTypesService, this.serviceContainer.policyService, + this.serviceContainer.billingAccountProfileStateService, ); this.generateCommand = new GenerateCommand( this.serviceContainer.passwordGenerationService, @@ -127,6 +133,13 @@ export class OssServeConfigurator { this.serviceContainer.accountService, this.serviceContainer.cliRestrictedItemTypesService, ); + this.archiveCommand = new ArchiveCommand( + this.serviceContainer.cipherService, + this.serviceContainer.accountService, + this.serviceContainer.configService, + this.serviceContainer.cipherArchiveService, + this.serviceContainer.billingAccountProfileStateService, + ); this.confirmCommand = new ConfirmCommand( this.serviceContainer.apiService, this.serviceContainer.keyService, @@ -140,6 +153,8 @@ export class OssServeConfigurator { this.serviceContainer.cipherService, this.serviceContainer.accountService, this.serviceContainer.cipherAuthorizationService, + this.serviceContainer.cipherArchiveService, + this.serviceContainer.configService, ); this.shareCommand = new ShareCommand( this.serviceContainer.cipherService, @@ -199,7 +214,7 @@ export class OssServeConfigurator { ); } - configureRouter(router: koaRouter) { + async configureRouter(router: koaRouter) { router.get("/generate", async (ctx, next) => { const response = await this.generateCommand.run(ctx.request.query); this.processResponse(ctx.response, response); @@ -401,6 +416,23 @@ export class OssServeConfigurator { this.processResponse(ctx.response, response); await next(); }); + + const isArchivedEnabled = await this.serviceContainer.configService.getFeatureFlag( + FeatureFlag.PM19148_InnovationArchive, + ); + + if (isArchivedEnabled) { + router.post("/archive/:object/:id", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + let response: Response = null; + response = await this.archiveCommand.run(ctx.params.object, ctx.params.id); + this.processResponse(ctx.response, response); + await next(); + }); + } } protected processResponse(res: koa.Response, commandResponse: Response) { diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 4d541739aab..8f202bc0845 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -5,6 +5,7 @@ import { program, Command, OptionValues } from "commander"; import { firstValueFrom, of, switchMap } from "rxjs"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LockCommand } from "./auth/commands/lock.command"; import { LoginCommand } from "./auth/commands/login.command"; @@ -26,6 +27,10 @@ const writeLn = CliUtils.writeLn; export class Program extends BaseProgram { async register() { + const isArchivedEnabled = await this.serviceContainer.configService.getFeatureFlag( + FeatureFlag.PM19148_InnovationArchive, + ); + program .option("--pretty", "Format output. JSON is tabbed with two spaces.") .option("--raw", "Return raw output instead of a descriptive message.") @@ -94,6 +99,9 @@ export class Program extends BaseProgram { " bw edit folder c7c7b60b-9c61-40f2-8ccd-36c49595ed72 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==", ); writeLn(" bw delete item 99ee88d2-6046-4ea7-92c2-acac464b1412"); + if (isArchivedEnabled) { + writeLn(" bw archive item 99ee88d2-6046-4ea7-92c2-acac464b1412"); + } writeLn(" bw generate -lusn --length 18"); writeLn(" bw config server https://bitwarden.example.com"); writeLn(" bw send -f ./file.ext"); diff --git a/apps/cli/src/register-oss-programs.ts b/apps/cli/src/register-oss-programs.ts index 1fc1f0119d2..71d7aaa0d52 100644 --- a/apps/cli/src/register-oss-programs.ts +++ b/apps/cli/src/register-oss-programs.ts @@ -15,7 +15,7 @@ export async function registerOssPrograms(serviceContainer: ServiceContainer) { await program.register(); const vaultProgram = new VaultProgram(serviceContainer); - vaultProgram.register(); + await vaultProgram.register(); const sendProgram = new SendProgram(serviceContainer); sendProgram.register(); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index c5e6dd881f6..9369fcc15cb 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -123,6 +123,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { @@ -130,6 +131,7 @@ import { DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service"; import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; @@ -302,6 +304,7 @@ export class ServiceContainer { cipherEncryptionService: CipherEncryptionService; restrictedItemTypesService: RestrictedItemTypesService; cliRestrictedItemTypesService: CliRestrictedItemTypesService; + cipherArchiveService: CipherArchiveService; constructor() { let p = null; @@ -729,6 +732,13 @@ export class ServiceContainer { this.messagingService, ); + this.cipherArchiveService = new DefaultCipherArchiveService( + this.cipherService, + this.apiService, + this.billingAccountProfileStateService, + this.configService, + ); + this.folderService = new FolderService( this.keyService, this.encryptService, diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 5b35f6b0499..21f87feab00 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { program, Command } from "commander"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + import { ConfirmCommand } from "./admin-console/commands/confirm.command"; import { ShareCommand } from "./admin-console/commands/share.command"; import { BaseProgram } from "./base-program"; @@ -13,25 +15,34 @@ import { Response } from "./models/response"; import { ExportCommand } from "./tools/export.command"; import { ImportCommand } from "./tools/import.command"; import { CliUtils } from "./utils"; +import { ArchiveCommand } from "./vault/archive.command"; import { CreateCommand } from "./vault/create.command"; import { DeleteCommand } from "./vault/delete.command"; const writeLn = CliUtils.writeLn; export class VaultProgram extends BaseProgram { - register() { + async register() { + const isArchivedEnabled = await this.serviceContainer.configService.getFeatureFlag( + FeatureFlag.PM19148_InnovationArchive, + ); + program - .addCommand(this.listCommand()) + .addCommand(this.listCommand(isArchivedEnabled)) .addCommand(this.getCommand()) .addCommand(this.createCommand()) .addCommand(this.editCommand()) .addCommand(this.deleteCommand()) - .addCommand(this.restoreCommand()) + .addCommand(this.restoreCommand(isArchivedEnabled)) .addCommand(this.shareCommand("move", false)) .addCommand(this.confirmCommand()) .addCommand(this.importCommand()) .addCommand(this.exportCommand()) .addCommand(this.shareCommand("share", true)); + + if (isArchivedEnabled) { + program.addCommand(this.archiveCommand()); + } } private validateObject(requestedObject: string, validObjects: string[]): boolean { @@ -42,7 +53,7 @@ export class VaultProgram extends BaseProgram { Response.badRequest( 'Unknown object "' + requestedObject + - '". Allowed objects are ' + + '". Allowed objects are: ' + validObjects.join(", ") + ".", ), @@ -51,7 +62,7 @@ export class VaultProgram extends BaseProgram { return success; } - private listCommand(): Command { + private listCommand(isArchivedEnabled: boolean): Command { const listObjects = [ "items", "folders", @@ -61,7 +72,7 @@ export class VaultProgram extends BaseProgram { "organizations", ]; - return new Command("list") + const command = new Command("list") .argument("", "Valid objects are: " + listObjects.join(", ")) .description("List an array of objects from the vault.") .option("--search ", "Perform a search on the listed objects.") @@ -94,6 +105,9 @@ export class VaultProgram extends BaseProgram { " bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2 --organizationid notnull", ); writeLn(" bw list items --trash"); + if (isArchivedEnabled) { + writeLn(" bw list items --archived"); + } writeLn(" bw list folders --search email"); writeLn(" bw list org-members --organizationid 60556c31-e649-4b5d-8daf-fc1c391a1bf2"); writeLn("", true); @@ -116,11 +130,18 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.accountService, this.serviceContainer.keyService, this.serviceContainer.cliRestrictedItemTypesService, + this.serviceContainer.cipherArchiveService, ); const response = await command.run(object, cmd); this.processResponse(response); }); + + if (isArchivedEnabled) { + command.option("--archived", "Filter items that are archived."); + } + + return command; } private getCommand(): Command { @@ -286,6 +307,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.accountService, this.serviceContainer.cliRestrictedItemTypesService, this.serviceContainer.policyService, + this.serviceContainer.billingAccountProfileStateService, ); const response = await command.run(object, id, encodedJson, cmd); this.processResponse(response); @@ -336,12 +358,41 @@ export class VaultProgram extends BaseProgram { }); } - private restoreCommand(): Command { + private archiveCommand(): Command { + const archiveObjects = ["item"]; + return new Command("archive") + .argument("", "Valid objects are: " + archiveObjects.join(", ")) + .argument("", "Object's globally unique `id`.") + .description("Archive an object from the vault.") + .on("--help", () => { + writeLn("\n Examples:"); + writeLn(""); + writeLn(" bw archive item 7063feab-4b10-472e-b64c-785e2b870b92"); + writeLn("", true); + }) + .action(async (object, id) => { + if (!this.validateObject(object, archiveObjects)) { + return; + } + + await this.exitIfLocked(); + const command = new ArchiveCommand( + this.serviceContainer.cipherService, + this.serviceContainer.accountService, + this.serviceContainer.configService, + this.serviceContainer.cipherArchiveService, + this.serviceContainer.billingAccountProfileStateService, + ); + const response = await command.run(object, id); + this.processResponse(response); + }); + } + + private restoreCommand(isArchivedEnabled: boolean): Command { const restoreObjects = ["item"]; - return new Command("restore") + const command = new Command("restore") .argument("", "Valid objects are: " + restoreObjects.join(", ")) .argument("", "Object's globally unique `id`.") - .description("Restores an object from the trash.") .on("--help", () => { writeLn("\n Examples:"); writeLn(""); @@ -358,10 +409,20 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.cipherService, this.serviceContainer.accountService, this.serviceContainer.cipherAuthorizationService, + this.serviceContainer.cipherArchiveService, + this.serviceContainer.configService, ); const response = await command.run(object, id); this.processResponse(response); }); + + if (isArchivedEnabled) { + command.description("Restores an object from the trash or archive."); + } else { + command.description("Restores an object from the trash."); + } + + return command; } private shareCommand(commandName: string, deprecated: boolean): Command { diff --git a/apps/cli/src/vault/archive.command.ts b/apps/cli/src/vault/archive.command.ts new file mode 100644 index 00000000000..5ced2282c6d --- /dev/null +++ b/apps/cli/src/vault/archive.command.ts @@ -0,0 +1,109 @@ +import { firstValueFrom } from "rxjs"; + +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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; +import { UserId } from "@bitwarden/user-core"; + +import { Response } from "../models/response"; + +export class ArchiveCommand { + constructor( + private cipherService: CipherService, + private accountService: AccountService, + private configService: ConfigService, + private cipherArchiveService: CipherArchiveService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + ) {} + + async run(object: string, id: string): Promise { + const featureFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM19148_InnovationArchive, + ); + + if (!featureFlagEnabled) { + return Response.notFound(); + } + + if (id != null) { + id = id.toLowerCase(); + } + + const normalizedObject = object.toLowerCase(); + + if (normalizedObject === "item") { + return this.archiveCipher(id); + } + + return Response.badRequest("Unknown object."); + } + + private async archiveCipher(cipherId: string) { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const cipher = await this.cipherService.get(cipherId, activeUserId); + + if (cipher == null) { + return Response.notFound(); + } + + const cipherView = await this.cipherService.decrypt(cipher, activeUserId); + + const { canArchive, errorMessage } = await this.userCanArchiveCipher(cipherView, activeUserId); + + if (!canArchive) { + return Response.error(errorMessage); + } + + try { + await this.cipherArchiveService.archiveWithServer(cipherView.id as CipherId, activeUserId); + return Response.success(); + } catch (e) { + return Response.error(e); + } + } + + /** + * Determines if the user can archive the given cipher. + * When the user cannot archive the cipher, an appropriate error message is provided. + */ + private async userCanArchiveCipher( + cipher: CipherView, + userId: UserId, + ): Promise< + { canArchive: true; errorMessage?: never } | { canArchive: false; errorMessage: string } + > { + const hasPremiumFromAnySource = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId), + ); + + switch (true) { + case !hasPremiumFromAnySource: { + return { + canArchive: false, + errorMessage: "Premium status is required to use this feature.", + }; + } + case CipherViewLikeUtils.isArchived(cipher): { + return { canArchive: false, errorMessage: "Item is already archived." }; + } + case CipherViewLikeUtils.isDeleted(cipher): { + return { + canArchive: false, + errorMessage: "Item is in the trash, the item must be restored before archiving.", + }; + } + case cipher.organizationId != null: { + return { canArchive: false, errorMessage: "Cannot archive items in an organization." }; + } + default: + return { canArchive: true }; + } + } +} diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 0892bb42214..03a205e9c48 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -20,6 +20,7 @@ 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"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { Folder } from "@bitwarden/common/vault/models/domain/folder"; import { KeyService } from "@bitwarden/key-management"; import { OrganizationCollectionRequest } from "../admin-console/models/request/organization-collection.request"; @@ -183,8 +184,8 @@ export class CreateCommand { const userKey = await this.keyService.getUserKey(activeUserId); const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey); try { - await this.folderApiService.save(folder, activeUserId); - const newFolder = await this.folderService.get(folder.id, activeUserId); + const folderData = await this.folderApiService.save(folder, activeUserId); + const newFolder = new Folder(folderData); const decFolder = await newFolder.decrypt(); const res = new FolderResponse(decFolder); return Response.success(res); diff --git a/apps/cli/webpack.base.js b/apps/cli/webpack.base.js new file mode 100644 index 00000000000..01d5fc5b175 --- /dev/null +++ b/apps/cli/webpack.base.js @@ -0,0 +1,124 @@ +const path = require("path"); +const webpack = require("webpack"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const nodeExternals = require("webpack-node-externals"); +const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); +const config = require("./config/config"); + +module.exports.getEnv = function getEnv() { + const ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV; + return { ENV }; +}; + +const DEFAULT_PARAMS = { + localesPath: "./src/locales", + modulesPath: [path.resolve("../../node_modules")], + externalsModulesDir: "../../node_modules", + outputPath: path.resolve(__dirname, "build"), + watch: false, +}; + +/** + * + * @param {{ + * configName: string; + * entry: string; + * tsConfig: string; + * outputPath?: string; + * mode?: string; + * env?: string; + * modulesPath?: string[]; + * localesPath?: string; + * externalsModulesDir?: string; + * watch?: boolean; + * }} params + */ +module.exports.buildConfig = function buildConfig(params) { + params = { ...DEFAULT_PARAMS, ...params }; + const ENV = params.env || module.exports.getEnv().ENV; + + const envConfig = config.load(ENV); + config.log(`Building CLI - ${params.configName} version`); + config.log(envConfig); + + const moduleRules = [ + { + test: /\.ts$/, + use: "ts-loader", + exclude: path.resolve(__dirname, "node_modules"), + }, + ]; + + const plugins = [ + new CopyWebpackPlugin({ + patterns: [{ from: params.localesPath, to: "locales" }], + }), + new webpack.DefinePlugin({ + "process.env.BWCLI_ENV": JSON.stringify(ENV), + }), + new webpack.BannerPlugin({ + banner: "#!/usr/bin/env node", + raw: true, + }), + new webpack.IgnorePlugin({ + resourceRegExp: /^encoding$/, + contextRegExp: /node-fetch/, + }), + new webpack.EnvironmentPlugin({ + ENV: ENV, + BWCLI_ENV: ENV, + FLAGS: envConfig.flags, + DEV_FLAGS: envConfig.devFlags, + }), + new webpack.IgnorePlugin({ + resourceRegExp: /canvas/, + contextRegExp: /jsdom$/, + }), + ]; + + const webpackConfig = { + mode: params.mode || ENV, + target: "node", + devtool: ENV === "development" ? "eval-source-map" : "source-map", + node: { + __dirname: false, + __filename: false, + }, + entry: { + bw: params.entry, + }, + optimization: { + minimize: false, + }, + resolve: { + extensions: [".ts", ".js"], + symlinks: false, + modules: params.modulesPath, + plugins: [new TsconfigPathsPlugin({ configFile: params.tsConfig })], + }, + output: { + filename: "[name].js", + path: path.resolve(params.outputPath), + clean: true, + }, + module: { rules: moduleRules }, + plugins: plugins, + externals: [ + nodeExternals({ + modulesDir: params.externalsModulesDir, + allowlist: [/@bitwarden/], + }), + ], + experiments: { + asyncWebAssembly: true, + }, + }; + if (params.watch) { + webpackConfig.watch = true; + webpackConfig.watchOptions = { + ignored: /node_modules/, + poll: 1000, + }; + } + return webpackConfig; +}; diff --git a/apps/cli/webpack.config.js b/apps/cli/webpack.config.js index d5f66af73ec..b8eae3dce4d 100644 --- a/apps/cli/webpack.config.js +++ b/apps/cli/webpack.config.js @@ -1,89 +1,48 @@ const path = require("path"); -const webpack = require("webpack"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const nodeExternals = require("webpack-node-externals"); -const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); -const config = require("./config/config"); +const { buildConfig } = require("./webpack.base"); -if (process.env.NODE_ENV == null) { - process.env.NODE_ENV = "development"; -} -const ENV = (process.env.ENV = process.env.NODE_ENV); +module.exports = (webpackConfig, context) => { + // Detect if called by Nx (context parameter exists) + const isNxBuild = context && context.options; -const envConfig = config.load(ENV); -config.log(envConfig); + if (isNxBuild) { + // Nx build configuration + const mode = context.options.mode || "development"; + if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = mode; + } + const ENV = (process.env.ENV = process.env.NODE_ENV); -const moduleRules = [ - { - test: /\.ts$/, - use: "ts-loader", - exclude: path.resolve(__dirname, "node_modules"), - }, -]; + return buildConfig({ + configName: "OSS", + entry: context.options.main || "apps/cli/src/bw.ts", + tsConfig: "tsconfig.base.json", + outputPath: path.resolve(context.context.root, context.options.outputPath), + mode: mode, + env: ENV, + modulesPath: [path.resolve("node_modules")], + localesPath: "apps/cli/src/locales", + externalsModulesDir: "node_modules", + watch: context.options.watch || false, + }); + } else { + // npm build configuration + if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = "development"; + } + const ENV = (process.env.ENV = process.env.NODE_ENV); + const mode = ENV; -const plugins = [ - new CopyWebpackPlugin({ - patterns: [{ from: "./src/locales", to: "locales" }], - }), - new webpack.DefinePlugin({ - "process.env.BWCLI_ENV": JSON.stringify(ENV), - }), - new webpack.BannerPlugin({ - banner: "#!/usr/bin/env node", - raw: true, - }), - new webpack.IgnorePlugin({ - resourceRegExp: /^encoding$/, - contextRegExp: /node-fetch/, - }), - new webpack.EnvironmentPlugin({ - ENV: ENV, - BWCLI_ENV: ENV, - FLAGS: envConfig.flags, - DEV_FLAGS: envConfig.devFlags, - }), - new webpack.IgnorePlugin({ - resourceRegExp: /canvas/, - contextRegExp: /jsdom$/, - }), -]; - -const webpackConfig = { - mode: ENV, - target: "node", - devtool: ENV === "development" ? "eval-source-map" : "source-map", - node: { - __dirname: false, - __filename: false, - }, - entry: { - bw: "./src/bw.ts", - }, - optimization: { - minimize: false, - }, - resolve: { - extensions: [".ts", ".js"], - symlinks: false, - modules: [path.resolve("../../node_modules")], - plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })], - }, - output: { - filename: "[name].js", - path: path.resolve(__dirname, "build"), - clean: true, - }, - module: { rules: moduleRules }, - plugins: plugins, - externals: [ - nodeExternals({ - modulesDir: "../../node_modules", - allowlist: [/@bitwarden/], - }), - ], - experiments: { - asyncWebAssembly: true, - }, + return buildConfig({ + configName: "OSS", + entry: "./src/bw.ts", + tsConfig: "./tsconfig.json", + outputPath: path.resolve(__dirname, "build"), + mode: mode, + env: ENV, + modulesPath: [path.resolve("../../node_modules")], + localesPath: "./src/locales", + externalsModulesDir: "../../node_modules", + }); + } }; - -module.exports = webpackConfig; diff --git a/apps/desktop/CLAUDE.md b/apps/desktop/CLAUDE.md new file mode 100644 index 00000000000..65b1952851a --- /dev/null +++ b/apps/desktop/CLAUDE.md @@ -0,0 +1,11 @@ +# Desktop (Electron) - Critical Rules + +- **CRITICAL**: Separate main process vs renderer process contexts + - Main process: Node.js + Electron APIs (files in `/apps/desktop/src/main/`) + - Renderer process: Browser-like environment (Angular app files) + - Use IPC (Inter-Process Communication) for cross-process communication + +- **NEVER** import Node.js modules directly in renderer process +- **NEVER** import Angular modules in the main process + - Use preload scripts or IPC to access Node.js functionality + - See `/apps/desktop/src/*/preload.ts` files for patterns diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index ca9965e5e18..9020e08362e 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -342,6 +342,7 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" name = "autotype" version = "0.0.0" dependencies = [ + "anyhow", "tracing", "windows 0.61.1", "windows-core 0.61.0", diff --git a/apps/desktop/desktop_native/autotype/Cargo.toml b/apps/desktop/desktop_native/autotype/Cargo.toml index ceccd0c890a..3d1e74254ce 100644 --- a/apps/desktop/desktop_native/autotype/Cargo.toml +++ b/apps/desktop/desktop_native/autotype/Cargo.toml @@ -5,6 +5,9 @@ license.workspace = true edition.workspace = true publish.workspace = true +[dependencies] +anyhow = { workspace = true } + [target.'cfg(windows)'.dependencies] tracing.workspace = true windows = { workspace = true, features = [ diff --git a/apps/desktop/desktop_native/autotype/src/lib.rs b/apps/desktop/desktop_native/autotype/src/lib.rs index f1aab2ba164..92996996434 100644 --- a/apps/desktop/desktop_native/autotype/src/lib.rs +++ b/apps/desktop/desktop_native/autotype/src/lib.rs @@ -1,3 +1,5 @@ +use anyhow::Result; + #[cfg_attr(target_os = "linux", path = "linux.rs")] #[cfg_attr(target_os = "macos", path = "macos.rs")] #[cfg_attr(target_os = "windows", path = "windows.rs")] @@ -5,18 +7,26 @@ mod windowing; /// Gets the title bar string for the foreground window. /// -/// TODO: The error handling will be improved in a future PR: PM-23615 -#[allow(clippy::result_unit_err)] -pub fn get_foreground_window_title() -> std::result::Result { +/// # Errors +/// +/// This function returns an `anyhow::Error` if there is any +/// issue obtaining the window title. Detailed reasons will +/// vary based on platform implementation. +pub fn get_foreground_window_title() -> Result { windowing::get_foreground_window_title() } /// Attempts to type the input text wherever the user's cursor is. /// -/// `input` must be an array of utf-16 encoded characters to insert. +/// # Arguments /// -/// TODO: The error handling will be improved in a future PR: PM-23615 -#[allow(clippy::result_unit_err)] -pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> std::result::Result<(), ()> { +/// * `input` must be an array of utf-16 encoded characters to insert. +/// +/// # Errors +/// +/// This function returns an `anyhow::Error` if there is any +/// issue obtaining the window title. Detailed reasons will +/// vary based on platform implementation. +pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> { windowing::type_input(input, keyboard_shortcut) } diff --git a/apps/desktop/desktop_native/autotype/src/linux.rs b/apps/desktop/desktop_native/autotype/src/linux.rs index 148b1aab6eb..9fda0ed9e33 100644 --- a/apps/desktop/desktop_native/autotype/src/linux.rs +++ b/apps/desktop/desktop_native/autotype/src/linux.rs @@ -1,10 +1,7 @@ -pub fn get_foreground_window_title() -> std::result::Result { +pub fn get_foreground_window_title() -> anyhow::Result { todo!("Bitwarden does not yet support Linux autotype"); } -pub fn type_input( - _input: Vec, - _keyboard_shortcut: Vec, -) -> std::result::Result<(), ()> { +pub fn type_input(_input: Vec, _keyboard_shortcut: Vec) -> anyhow::Result<()> { todo!("Bitwarden does not yet support Linux autotype"); } diff --git a/apps/desktop/desktop_native/autotype/src/macos.rs b/apps/desktop/desktop_native/autotype/src/macos.rs index 5542e7a3a6b..c6681a3291e 100644 --- a/apps/desktop/desktop_native/autotype/src/macos.rs +++ b/apps/desktop/desktop_native/autotype/src/macos.rs @@ -1,10 +1,7 @@ -pub fn get_foreground_window_title() -> std::result::Result { +pub fn get_foreground_window_title() -> anyhow::Result { todo!("Bitwarden does not yet support macOS autotype"); } -pub fn type_input( - _input: Vec, - _keyboard_shortcut: Vec, -) -> std::result::Result<(), ()> { +pub fn type_input(_input: Vec, _keyboard_shortcut: Vec) -> anyhow::Result<()> { todo!("Bitwarden does not yet support macOS autotype"); } diff --git a/apps/desktop/desktop_native/autotype/src/windows.rs b/apps/desktop/desktop_native/autotype/src/windows.rs index 1d39d3f7ae5..1e125ef8e21 100644 --- a/apps/desktop/desktop_native/autotype/src/windows.rs +++ b/apps/desktop/desktop_native/autotype/src/windows.rs @@ -1,38 +1,141 @@ -use std::ffi::OsString; -use std::os::windows::ffi::OsStringExt; +use std::{ffi::OsString, os::windows::ffi::OsStringExt}; -use tracing::debug; -use windows::Win32::Foundation::{GetLastError, HWND}; -use windows::Win32::UI::Input::KeyboardAndMouse::{ - SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_UNICODE, - VIRTUAL_KEY, -}; -use windows::Win32::UI::WindowsAndMessaging::{ - GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW, +use anyhow::{anyhow, Result}; +use tracing::{debug, error, warn}; +use windows::Win32::{ + Foundation::{GetLastError, SetLastError, HWND, WIN32_ERROR}, + UI::{ + Input::KeyboardAndMouse::{ + SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, + KEYEVENTF_UNICODE, VIRTUAL_KEY, + }, + WindowsAndMessaging::{GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW}, + }, }; +const WIN32_SUCCESS: WIN32_ERROR = WIN32_ERROR(0); + +fn clear_last_error() { + debug!("Clearing last error with SetLastError."); + unsafe { + SetLastError(WIN32_ERROR(0)); + } +} + +fn get_last_error() -> WIN32_ERROR { + let last_err = unsafe { GetLastError() }; + debug!("GetLastError(): {}", last_err.to_hresult().message()); + last_err +} + +// The handle should be validated before any unsafe calls referencing it. +fn validate_window_handle(handle: &HWND) -> Result<()> { + if handle.is_invalid() { + error!("Window handle is invalid."); + return Err(anyhow!("Window handle is invalid.")); + } + Ok(()) +} + +// ---------- Window title -------------- + /// Gets the title bar string for the foreground window. -pub fn get_foreground_window_title() -> std::result::Result { - let Ok(window_handle) = get_foreground_window() else { - return Err(()); - }; - let Ok(Some(window_title)) = get_window_title(window_handle) else { - return Err(()); - }; +pub fn get_foreground_window_title() -> Result { + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow + let window_handle = unsafe { GetForegroundWindow() }; - Ok(window_title) + debug!("GetForegroundWindow() called."); + + validate_window_handle(&window_handle)?; + + get_window_title(&window_handle) } +/// Gets the length of the window title bar text. +/// +/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw +fn get_window_title_length(window_handle: &HWND) -> Result { + // GetWindowTextLengthW does not itself clear the last error so we must do it ourselves. + clear_last_error(); + + validate_window_handle(window_handle)?; + + let length = unsafe { GetWindowTextLengthW(*window_handle) }; + + let length = usize::try_from(length)?; + + debug!(length, "window text length retrieved from handle."); + + if length == 0 { + // attempt to retreive win32 error + let last_err = get_last_error(); + if last_err != WIN32_SUCCESS { + let last_err = last_err.to_hresult().message(); + error!(last_err, "Error getting window text length."); + return Err(anyhow!("Error getting window text length: {last_err}")); + } + } + + Ok(length) +} + +/// Gets the window title bar title. +/// +/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw +fn get_window_title(window_handle: &HWND) -> Result { + let expected_window_title_length = get_window_title_length(window_handle)?; + + // This isn't considered an error by the windows API, but in practice it means we can't + // match against the title so we'll stop here. + // The upstream will make a contains comparison on what we return, so an empty string + // will not result on a match. + if expected_window_title_length == 0 { + warn!("Window title length is zero."); + return Ok(String::from("")); + } + + let mut buffer: Vec = vec![0; expected_window_title_length + 1]; // add extra space for the null character + + validate_window_handle(window_handle)?; + + let actual_window_title_length = unsafe { GetWindowTextW(*window_handle, &mut buffer) }; + + debug!(actual_window_title_length, "window title retrieved."); + + if actual_window_title_length == 0 { + // attempt to retreive win32 error + let last_err = get_last_error(); + if last_err != WIN32_SUCCESS { + let last_err = last_err.to_hresult().message(); + error!(last_err, "Error retrieving window title."); + return Err(anyhow!("Error retrieving window title. {last_err}")); + } + // in practice, we should not get to the below code, since we asserted the len > 0 + // above. but it is an extra protection in case the windows API didn't set an error. + warn!(expected_window_title_length, "No window title retrieved."); + } + + let window_title = OsString::from_wide(&buffer); + + Ok(window_title.to_string_lossy().into_owned()) +} + +// ---------- Type Input -------------- + /// Attempts to type the input text wherever the user's cursor is. /// /// `input` must be a vector of utf-16 encoded characters to insert. /// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super, Shift, letters a - Z /// /// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput -pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<(), ()> { +pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> { const TAB_KEY: u8 = 9; - let mut keyboard_inputs: Vec = Vec::new(); + // the length of this vec is always shortcut keys to release + (2x length of input chars) + let mut keyboard_inputs: Vec = + Vec::with_capacity(keyboard_shortcut.len() + (input.len() * 2)); + + debug!(?keyboard_shortcut, "Converting keyboard shortcut to input."); // Add key "up" inputs for the shortcut for key in keyboard_shortcut { @@ -63,7 +166,7 @@ pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<(), /// Converts a valid shortcut key to an "up" keyboard input. /// /// `input` must be a valid shortcut key: Control, Alt, Super, Shift, letters [a-z][A-Z] -fn convert_shortcut_key_to_up_input(key: String) -> Result { +fn convert_shortcut_key_to_up_input(key: String) -> Result { const SHIFT_KEY: u8 = 0x10; const SHIFT_KEY_STR: &str = "Shift"; const CONTROL_KEY: u8 = 0x11; @@ -89,9 +192,15 @@ fn convert_shortcut_key_to_up_input(key: String) -> Result { /// Because we only accept [a-z][A-Z], the decimal u16 /// cast of the letter is safe because the unicode code point /// of these characters fits in a u16. -fn get_alphabetic_hotkey(letter: String) -> Result { +fn get_alphabetic_hotkey(letter: String) -> Result { if letter.len() != 1 { - return Err(()); + error!( + len = letter.len(), + "Final keyboard shortcut key should be a single character." + ); + return Err(anyhow!( + "Final keyboard shortcut key should be a single character: {letter}" + )); } let c = letter.chars().next().expect("letter is size 1"); @@ -99,65 +208,20 @@ fn get_alphabetic_hotkey(letter: String) -> Result { // is_ascii_alphabetic() checks for: // U+0041 `A` ..= U+005A `Z`, or U+0061 `a` ..= U+007A `z` if !c.is_ascii_alphabetic() { - return Err(()); + error!(letter = %c, "Letter is not ASCII Alphabetic ([a-z][A-Z])."); + return Err(anyhow!( + "Letter is not ASCII Alphabetic ([a-z][A-Z]): '{letter}'", + )); } - Ok(c as u16) + let c = c as u16; + + debug!(c, letter, "Got alphabetic hotkey."); + + Ok(c) } -/// Gets the foreground window handle. -/// -/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow -fn get_foreground_window() -> Result { - let foreground_window_handle = unsafe { GetForegroundWindow() }; - - if foreground_window_handle.is_invalid() { - return Err(()); - } - - Ok(foreground_window_handle) -} - -/// Gets the length of the window title bar text. -/// -/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw -fn get_window_title_length(window_handle: HWND) -> Result { - if window_handle.is_invalid() { - return Err(()); - } - - match usize::try_from(unsafe { GetWindowTextLengthW(window_handle) }) { - Ok(length) => Ok(length), - Err(_) => Err(()), - } -} - -/// Gets the window title bar title. -/// -/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw -fn get_window_title(window_handle: HWND) -> Result, ()> { - if window_handle.is_invalid() { - return Err(()); - } - - let window_title_length = get_window_title_length(window_handle)?; - if window_title_length == 0 { - return Ok(None); - } - - let mut buffer: Vec = vec![0; window_title_length + 1]; // add extra space for the null character - - let window_title_length = unsafe { GetWindowTextW(window_handle, &mut buffer) }; - if window_title_length == 0 { - return Ok(None); - } - - let window_title = OsString::from_wide(&buffer); - - Ok(Some(window_title.to_string_lossy().into_owned())) -} - -/// Used in build_input() to specify if an input key is being pressed (down) or released (up). +/// An input key can be either pressed (down), or released (up). enum InputKeyPress { Down, Up, @@ -233,18 +297,29 @@ fn build_virtual_key_input(key_press: InputKeyPress, virtual_key: u8) -> INPUT { /// Attempts to type the provided input wherever the user's cursor is. /// /// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput -fn send_input(inputs: Vec) -> Result<(), ()> { +fn send_input(inputs: Vec) -> Result<()> { let insert_count = unsafe { SendInput(&inputs, std::mem::size_of::() as i32) }; - let e = unsafe { GetLastError().to_hresult().message() }; - debug!("type_input() called, GetLastError() is: {:?}", e); + debug!("SendInput() called."); if insert_count == 0 { - return Err(()); // input was blocked by another thread + let last_err = get_last_error().to_hresult().message(); + error!(GetLastError = %last_err, "SendInput sent 0 inputs. Input was blocked by another thread."); + + return Err(anyhow!("SendInput sent 0 inputs. Input was blocked by another thread. GetLastError: {last_err}")); } else if insert_count != inputs.len() as u32 { - return Err(()); // input insertion not completed + let last_err = get_last_error().to_hresult().message(); + error!(sent = %insert_count, expected = inputs.len(), GetLastError = %last_err, + "SendInput sent does not match expected." + ); + return Err(anyhow!( + "SendInput does not match expected. sent: {insert_count}, expected: {}", + inputs.len() + )); } + debug!(insert_count, "Autotype sent input."); + Ok(()) } @@ -263,16 +338,16 @@ mod tests { } #[test] - #[should_panic = ""] + #[should_panic = "Final keyboard shortcut key should be a single character: foo"] fn get_alphabetic_hot_key_fail_not_single_char() { let letter = String::from("foo"); get_alphabetic_hotkey(letter).unwrap(); } #[test] - #[should_panic = ""] + #[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '}'"] fn get_alphabetic_hot_key_fail_not_alphabetic() { - let letter = String::from("🚀"); + let letter = String::from("}"); get_alphabetic_hotkey(letter).unwrap(); } } diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs index d038ba2277f..3440a0114ae 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -11,6 +11,7 @@ use bitwarden_russh::{ session_bind::SessionBindResult, ssh_agent::{self, SshKey}, }; +use tracing::{error, info}; #[cfg_attr(target_os = "windows", path = "windows.rs")] #[cfg_attr(target_os = "macos", path = "unix.rs")] @@ -86,7 +87,7 @@ impl ssh_agent::Agent info: &peerinfo::models::PeerInfo, ) -> bool { if !self.is_running() { - println!("[BitwardenDesktopAgent] Agent is not running, but tried to call confirm"); + error!("Agent is not running, but tried to call confirm"); return false; } @@ -94,7 +95,7 @@ impl ssh_agent::Agent let request_data = match request_parser::parse_request(data) { Ok(data) => data, Err(e) => { - println!("[SSH Agent] Error while parsing request: {e}"); + error!(error = %e, "Error while parsing request"); return false; } }; @@ -105,12 +106,12 @@ impl ssh_agent::Agent _ => None, }; - println!( - "[SSH Agent] Confirming request from application: {}, is_forwarding: {}, namespace: {}, host_key: {}", + info!( + is_forwarding = %info.is_forwarding(), + namespace = ?namespace.as_ref(), + host_key = %STANDARD.encode(info.host_key()), + "Confirming request from application: {}", info.process_name(), - info.is_forwarding(), - namespace.clone().unwrap_or_default(), - STANDARD.encode(info.host_key()) ); let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe(); @@ -172,7 +173,7 @@ impl ssh_agent::Agent connection_info.set_host_key(session_bind_info.host_key.clone()); } SessionBindResult::SignatureFailure => { - println!("[BitwardenDesktopAgent] Session bind failure: Signature failure"); + error!("Session bind failure: Signature failure"); } } } @@ -181,7 +182,7 @@ impl ssh_agent::Agent impl BitwardenDesktopAgent { pub fn stop(&self) { if !self.is_running() { - println!("[BitwardenDesktopAgent] Tried to stop agent while it is not running"); + error!("Tried to stop agent while it is not running"); return; } @@ -227,7 +228,7 @@ impl BitwardenDesktopAgent { ); } Err(e) => { - eprintln!("[SSH Agent Native Module] Error while parsing key: {e}"); + error!(error=%e, "Error while parsing key"); } } } @@ -265,7 +266,7 @@ impl BitwardenDesktopAgent { fn get_request_id(&self) -> u32 { if !self.is_running() { - println!("[BitwardenDesktopAgent] Agent is not running, but tried to get request id"); + error!("Agent is not running, but tried to get request id"); return 0; } diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs index fccd7ca5ed6..cb10e873a33 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs @@ -14,6 +14,7 @@ use tokio::{ select, }; use tokio_util::sync::CancellationToken; +use tracing::{error, info}; use windows::Win32::{Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId}; use crate::ssh_agent::peerinfo::{self, models::PeerInfo}; @@ -31,42 +32,38 @@ impl NamedPipeServerStream { pub fn new(cancellation_token: CancellationToken, is_running: Arc) -> Self { let (tx, rx) = tokio::sync::mpsc::channel(16); tokio::spawn(async move { - println!( - "[SSH Agent Native Module] Creating named pipe server on {}", - PIPE_NAME - ); + info!("Creating named pipe server on {}", PIPE_NAME); let mut listener = match ServerOptions::new().create(PIPE_NAME) { Ok(pipe) => pipe, - Err(err) => { - println!("[SSH Agent Native Module] Encountered an error creating the first pipe. The system's openssh service must likely be disabled"); - println!("[SSH Agent Natvie Module] error: {}", err); + Err(e) => { + error!(error = %e, "Encountered an error creating the first pipe. The system's openssh service must likely be disabled"); cancellation_token.cancel(); is_running.store(false, Ordering::Relaxed); return; } }; loop { - println!("[SSH Agent Native Module] Waiting for connection"); + info!("Waiting for connection"); select! { _ = cancellation_token.cancelled() => { - println!("[SSH Agent Native Module] Cancellation token triggered, stopping named pipe server"); + info!("[SSH Agent Native Module] Cancellation token triggered, stopping named pipe server"); break; } _ = listener.connect() => { - println!("[SSH Agent Native Module] Incoming connection"); + info!("[SSH Agent Native Module] Incoming connection"); let handle = HANDLE(listener.as_raw_handle()); let mut pid = 0; unsafe { if let Err(e) = GetNamedPipeClientProcessId(handle, &mut pid) { - println!("Error getting named pipe client process id {}", e); + error!(error = %e, pid, "Faile to get named pipe client process id"); continue } }; let peer_info = peerinfo::gather::get_peer_info(pid); let peer_info = match peer_info { - Err(err) => { - println!("Failed getting process info for pid {} {}", pid, err); + Err(e) => { + error!(error = %e, pid = %pid, "Failed getting process info"); continue }, Ok(info) => info, @@ -76,8 +73,8 @@ impl NamedPipeServerStream { listener = match ServerOptions::new().create(PIPE_NAME) { Ok(pipe) => pipe, - Err(err) => { - println!("[SSH Agent Native Module] Encountered an error creating a new pipe {}", err); + Err(e) => { + error!(error = %e, "Encountered an error creating a new pipe"); cancellation_token.cancel(); is_running.store(false, Ordering::Relaxed); return; diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs index 53142d4c476..813ebd61cc1 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs @@ -12,6 +12,7 @@ use bitwarden_russh::ssh_agent; use homedir::my_home; use tokio::{net::UnixListener, sync::Mutex}; use tokio_util::sync::CancellationToken; +use tracing::{error, info}; use crate::ssh_agent::peercred_unix_listener_stream::PeercredUnixListenerStream; @@ -36,14 +37,12 @@ impl BitwardenDesktopAgent { let ssh_path = match std::env::var("BITWARDEN_SSH_AUTH_SOCK") { Ok(path) => path, Err(_) => { - println!("[SSH Agent Native Module] BITWARDEN_SSH_AUTH_SOCK not set, using default path"); + info!("BITWARDEN_SSH_AUTH_SOCK not set, using default path"); let ssh_agent_directory = match my_home() { Ok(Some(home)) => home, _ => { - println!( - "[SSH Agent Native Module] Could not determine home directory" - ); + info!("Could not determine home directory"); return; } }; @@ -65,10 +64,10 @@ impl BitwardenDesktopAgent { } }; - println!("[SSH Agent Native Module] Starting SSH Agent server on {ssh_path:?}"); + info!(socket = %ssh_path, "Starting SSH Agent server"); let sockname = std::path::Path::new(&ssh_path); if let Err(e) = std::fs::remove_file(sockname) { - println!("[SSH Agent Native Module] Could not remove existing socket file: {e}"); + error!(error = %e, socket = %ssh_path, "Could not remove existing socket file"); if e.kind() != std::io::ErrorKind::NotFound { return; } @@ -79,7 +78,7 @@ impl BitwardenDesktopAgent { // Only the current user should be able to access the socket if let Err(e) = fs::set_permissions(sockname, fs::Permissions::from_mode(0o600)) { - println!("[SSH Agent Native Module] Could not set socket permissions: {e}"); + error!(error = %e, socket = ?sockname, "Could not set socket permissions"); return; } @@ -100,10 +99,10 @@ impl BitwardenDesktopAgent { cloned_agent_state .is_running .store(false, std::sync::atomic::Ordering::Relaxed); - println!("[SSH Agent Native Module] SSH Agent server exited"); + info!("SSH Agent server exited"); } Err(e) => { - eprintln!("[SSH Agent Native Module] Error while starting agent server: {e}"); + error!(error = %e, socket = %ssh_path, "Unable to start start agent server"); } } }); diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 4d0dac1242a..ab9a162f8c5 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": "36.8.1", + "electronVersion": "38.2.0", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", diff --git a/apps/desktop/installer.nsh b/apps/desktop/installer.nsh index f8939423c8d..3fe8a69b089 100644 --- a/apps/desktop/installer.nsh +++ b/apps/desktop/installer.nsh @@ -7,3 +7,14 @@ ${endif} ${endif} !macroend + +# When the user is uninstalling the app, remove the auto-start registry entries +!macro customUnInstall + ${ifNot} ${isUpdated} + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "electron.app.${PRODUCT_NAME}" + DeleteRegValue HKLM "Software\Microsoft\Windows\CurrentVersion\Run" "electron.app.${PRODUCT_NAME}" + + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" "electron.app.${PRODUCT_NAME}" + DeleteRegValue HKLM "Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" "electron.app.${PRODUCT_NAME}" + ${endIf} +!macroend diff --git a/apps/desktop/package.json b/apps/desktop/package.json index fc3e7d0cad3..ce4c91f0d8c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2025.9.1", + "version": "2025.10.0", "keywords": [ "bitwarden", "password", @@ -21,18 +21,18 @@ "build-native": "cd desktop_native && node build.js", "build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"", "build:dev": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\" \"npm run build:preload:dev\"", - "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js", - "build:preload:dev": "cross-env NODE_ENV=development webpack --config webpack.preload.js", - "build:preload:watch": "cross-env NODE_ENV=development webpack --config webpack.preload.js --watch", + "build:preload": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name preload", + "build:preload:dev": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name preload", + "build:preload:watch": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name preload --watch", "build:macos-extension:mac": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mac", "build:macos-extension:mas": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mas", "build:macos-extension:masdev": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mas-dev", - "build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js", - "build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js", - "build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js --watch", - "build:renderer": "cross-env NODE_ENV=production webpack --config webpack.renderer.js", - "build:renderer:dev": "cross-env NODE_ENV=development webpack --config webpack.renderer.js", - "build:renderer:watch": "cross-env NODE_ENV=development webpack --config webpack.renderer.js --watch", + "build:main": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name main", + "build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.config.js --config-name main", + "build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.config.js --config-name main --watch", + "build:renderer": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name renderer", + "build:renderer:dev": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name renderer", + "build:renderer:watch": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name renderer --watch", "electron": "node ./scripts/start.js", "electron:ignore": "node ./scripts/start.js --ignore-certificate-errors", "clean:dist": "rimraf ./dist", diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index a0380a8b5ce..dfddff034e6 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -350,14 +350,16 @@ {{ "important" | i18n }} - {{ "enableAutotypeDescriptionTransitionKey" | i18n }} - {{ "editShortcut" | i18n }} -
diff --git a/apps/desktop/src/autofill/services/desktop-autotype-policy.service.spec.ts b/apps/desktop/src/autofill/services/desktop-autotype-policy.service.spec.ts index 7fb30333e28..555e6ceef5b 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype-policy.service.spec.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype-policy.service.spec.ts @@ -1,8 +1,10 @@ import { TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom, take, timeout, TimeoutError } from "rxjs"; +import { BehaviorSubject, firstValueFrom, take } from "rxjs"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -18,10 +20,10 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => { let policyService: MockProxy; let configService: MockProxy; - let mockAccountSubject: BehaviorSubject<{ id: UserId } | null>; + let mockAccountSubject: BehaviorSubject; let mockFeatureFlagSubject: BehaviorSubject; let mockAuthStatusSubject: BehaviorSubject; - let mockPolicyAppliesSubject: BehaviorSubject; + let mockPoliciesSubject: BehaviorSubject; const mockUserId = "user-123" as UserId; @@ -36,7 +38,7 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => { mockAuthStatusSubject = new BehaviorSubject( AuthenticationStatus.Unlocked, ); - mockPolicyAppliesSubject = new BehaviorSubject(false); + mockPoliciesSubject = new BehaviorSubject([]); accountService = mock(); authService = mock(); @@ -50,9 +52,7 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => { authService.authStatusFor$ = jest .fn() .mockImplementation((_: UserId) => mockAuthStatusSubject.asObservable()); - policyService.policyAppliesToUser$ = jest - .fn() - .mockReturnValue(mockPolicyAppliesSubject.asObservable()); + policyService.policies$ = jest.fn().mockReturnValue(mockPoliciesSubject.asObservable()); TestBed.configureTestingModule({ providers: [ @@ -72,7 +72,7 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => { mockAccountSubject.complete(); mockFeatureFlagSubject.complete(); mockAuthStatusSubject.complete(); - mockPolicyAppliesSubject.complete(); + mockPoliciesSubject.complete(); }); describe("autotypeDefaultSetting$", () => { @@ -82,11 +82,20 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => { expect(result).toBeNull(); }); - it("should not emit when no active account", async () => { + it("does not emit until an account appears", async () => { mockAccountSubject.next(null); - await expect( - firstValueFrom(service.autotypeDefaultSetting$.pipe(timeout({ first: 30 }))), - ).rejects.toBeInstanceOf(TimeoutError); + + mockAccountSubject.next({ id: mockUserId } as Account); + mockAuthStatusSubject.next(AuthenticationStatus.Unlocked); + mockPoliciesSubject.next([ + { + type: PolicyType.AutotypeDefaultSetting, + enabled: true, + } as Policy, + ]); + + const result = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(result).toBe(true); }); it("should emit null when user is not unlocked", async () => { @@ -96,34 +105,56 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => { }); it("should emit null when no autotype policy exists", async () => { - mockPolicyAppliesSubject.next(false); + mockPoliciesSubject.next([]); const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); expect(policy).toBeNull(); }); it("should emit true when autotype policy is enabled", async () => { - mockPolicyAppliesSubject.next(true); + mockPoliciesSubject.next([ + { + type: PolicyType.AutotypeDefaultSetting, + enabled: true, + } as Policy, + ]); const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); expect(policyStatus).toBe(true); }); - it("should emit false when autotype policy is disabled", async () => { - mockPolicyAppliesSubject.next(false); + it("should emit null when autotype policy is disabled", async () => { + mockPoliciesSubject.next([ + { + type: PolicyType.AutotypeDefaultSetting, + enabled: false, + } as Policy, + ]); const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); expect(policyStatus).toBeNull(); }); it("should emit null when autotype policy does not apply", async () => { - mockPolicyAppliesSubject.next(false); + mockPoliciesSubject.next([ + { + type: PolicyType.RequireSso, + enabled: true, + } as Policy, + ]); const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); expect(policy).toBeNull(); }); it("should react to authentication status changes", async () => { + mockPoliciesSubject.next([ + { + type: PolicyType.AutotypeDefaultSetting, + enabled: true, + } as Policy, + ]); + // Expect one emission when unlocked mockAuthStatusSubject.next(AuthenticationStatus.Unlocked); const first = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); - expect(first).toBeNull(); + expect(first).toBe(true); // Expect null emission when locked mockAuthStatusSubject.next(AuthenticationStatus.Locked); @@ -134,33 +165,131 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => { it("should react to account changes", async () => { const newUserId = "user-456" as UserId; + mockPoliciesSubject.next([ + { + type: PolicyType.AutotypeDefaultSetting, + enabled: true, + } as Policy, + ]); + // First value for original user const firstValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); - expect(firstValue).toBeNull(); + expect(firstValue).toBe(true); // Change account and expect a new emission mockAccountSubject.next({ id: newUserId, - }); + } as Account); const secondValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); - expect(secondValue).toBeNull(); + expect(secondValue).toBe(true); // Verify the auth lookup was switched to the new user expect(authService.authStatusFor$).toHaveBeenCalledWith(newUserId); + expect(policyService.policies$).toHaveBeenCalledWith(newUserId); }); it("should react to policy changes", async () => { - mockPolicyAppliesSubject.next(false); + mockPoliciesSubject.next([]); const nullValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); expect(nullValue).toBeNull(); - mockPolicyAppliesSubject.next(true); + mockPoliciesSubject.next([ + { + type: PolicyType.AutotypeDefaultSetting, + enabled: true, + } as Policy, + ]); const trueValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); expect(trueValue).toBe(true); - mockPolicyAppliesSubject.next(false); + mockPoliciesSubject.next([]); const nullValueAgain = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); expect(nullValueAgain).toBeNull(); }); + + it("emits null again if the feature flag turns off after emitting", async () => { + mockPoliciesSubject.next([ + { type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy, + ]); + expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBe(true); + + mockFeatureFlagSubject.next(false); + expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBeNull(); + }); + + it("replays the latest value to late subscribers", async () => { + mockPoliciesSubject.next([ + { type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy, + ]); + + await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + + const late = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(late).toBe(true); + }); + + it("does not re-emit when effective value is unchanged", async () => { + mockAccountSubject.next({ id: mockUserId } as Account); + mockAuthStatusSubject.next(AuthenticationStatus.Unlocked); + + const policies = [ + { + type: PolicyType.AutotypeDefaultSetting, + enabled: true, + } as Policy, + ]; + + mockPoliciesSubject.next(policies); + const first = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(first).toBe(true); + + let emissionCount = 0; + const subscription = service.autotypeDefaultSetting$.subscribe(() => { + emissionCount++; + }); + + mockPoliciesSubject.next(policies); + + await new Promise((resolve) => setTimeout(resolve, 50)); + subscription.unsubscribe(); + + expect(emissionCount).toBe(1); + }); + + it("does not emit policy values while locked; emits after unlocking", async () => { + mockAuthStatusSubject.next(AuthenticationStatus.Locked); + mockPoliciesSubject.next([ + { type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy, + ]); + + expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBeNull(); + + mockAuthStatusSubject.next(AuthenticationStatus.Unlocked); + expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBe(true); + }); + + it("emits correctly if auth unlocks before policies arrive", async () => { + mockAccountSubject.next({ id: mockUserId } as Account); + mockAuthStatusSubject.next(AuthenticationStatus.Unlocked); + mockPoliciesSubject.next([ + { + type: PolicyType.AutotypeDefaultSetting, + enabled: true, + } as Policy, + ]); + + const result = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + expect(result).toBe(true); + }); + + it("wires dependencies with initial user id", async () => { + mockPoliciesSubject.next([ + { type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy, + ]); + await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1))); + + expect(authService.authStatusFor$).toHaveBeenCalledWith(mockUserId); + expect(policyService.policies$).toHaveBeenCalledWith(mockUserId); + }); }); }); diff --git a/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts b/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts index 887a30ef6f6..d3ae67d2c8d 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype-policy.service.ts @@ -34,7 +34,7 @@ export class DesktopAutotypeDefaultSettingPolicy { } return this.accountService.activeAccount$.pipe( - filter((account) => account != null), + filter((account) => account != null && account.id != null), getUserId, distinctUntilChanged(), switchMap((userId) => { @@ -43,13 +43,16 @@ export class DesktopAutotypeDefaultSettingPolicy { distinctUntilChanged(), ); - const policy$ = this.policyService - .policyAppliesToUser$(PolicyType.AutotypeDefaultSetting, userId) - .pipe( - map((appliesToUser) => (appliesToUser ? true : null)), - distinctUntilChanged(), - shareReplay({ bufferSize: 1, refCount: true }), - ); + const policy$ = this.policyService.policies$(userId).pipe( + map((policies) => { + const autotypePolicy = policies.find( + (policy) => policy.type === PolicyType.AutotypeDefaultSetting && policy.enabled, + ); + return autotypePolicy ? true : null; + }), + distinctUntilChanged(), + shareReplay({ bufferSize: 1, refCount: true }), + ); return isUnlocked$.pipe(switchMap((unlocked) => (unlocked ? policy$ : of(null)))); }), diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.ts index 34f70be64cb..24ec3907a62 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.ts @@ -1,6 +1,6 @@ import { combineLatest, filter, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; @@ -73,7 +73,9 @@ export class DesktopAutotypeService { async init() { this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$; - this.autotypeKeyboardShortcut$ = this.autotypeKeyboardShortcut.state$; + this.autotypeKeyboardShortcut$ = this.autotypeKeyboardShortcut.state$.pipe( + map((shortcut) => shortcut ?? defaultWindowsAutotypeKeyboardShortcut), + ); // Currently Autotype is only supported for Windows if (this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop) { @@ -109,9 +111,9 @@ export class DesktopAutotypeService { switchMap((userId) => this.authService.authStatusFor$(userId)), ), this.accountService.activeAccount$.pipe( - map((activeAccount) => activeAccount?.id), - switchMap((userId) => - this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId), + filter((account): account is Account => !!account), + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ), ), ]).pipe( diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index e579c498ded..5da05806006 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Ongeldige hoofwagwoord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Tweestapsaantekening maak u rekening veiliger deur u aantekenpoging te bevestig met ’n ander toestel soos ’n beveiligingsleutel, waarmerktoep, SMS, telefoonoproep of e-pos. U kan tweestapsaantekening in die webkluis op bitwarden.com aktiveer. Wil u die webwerf nou besoek?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Vergrendel" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 4efec524886..2b1c70f30b6 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "كلمة المرور الرئيسية غير صالحة" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "تسجيل الدخول بخطوتين يجعل حسابك أكثر أمنا من خلال مطالبتك بالتحقق من تسجيل الدخول باستخدام جهاز آخر مثل مفتاح الأمان، تطبيق المصادقة، الرسائل القصيرة، المكالمة الهاتفية، أو البريد الإلكتروني. يمكن تمكين تسجيل الدخول بخطوتين على خزانة الويب bitwarden.com. هل تريد زيارة الموقع الآن؟" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "مقفل" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 9618bafca3e..bdb3f89b422 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Yararsız ana parol" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "İki addımlı giriş, güvənlik açarı, kimlik doğrulayıcı tətbiq, SMS, telefon zəngi və ya e-poçt kimi digər cihazlarla girişinizi doğrulamanızı tələb edərək hesabınızı daha da güvənli edir. İki addımlı giriş, bitwarden.com veb seyfində qurula bilər. Veb saytı indi ziyarət etmək istəyirsiniz?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Kilidli" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Daha az göstər" }, - "enableAutotype": { - "message": "Avto-yazmanı fəallaşdır" - }, "enableAutotypeDescription": { "message": "Bitwarden, giriş yerlərini doğrulamır, qısayolu istifadə etməzdən əvvəl doğru pəncərədə və xanada olduğunuza əmin olun." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "Daha çox naviqasiya yolu", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Təsdiqlə" }, - "enableAutotypeTransitionKey": { - "message": "Avto-yazma qısayolunu fəallaşdır" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Veriləri yanlış yerə doldurmamaq üçün qısayolu istifadə etməzdən əvvəl doğru xanada olduğunuza əmin olun." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Qısayola düzəliş et" }, - "archive": { - "message": "Arxivlə" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Arxivdən çıxart" diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index eb5971c97af..0664c25d84e 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Памылковы асноўны пароль" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Двухэтапны ўваход робіць ваш уліковы запіс больш бяспечным, патрабуючы пацвярджэнне ўваходу на іншай прыладзе з выкарыстаннем ключа бяспекі, праграмы аўтэнтыфікацыі, SMS, тэлефоннага званка або электроннай пошты. Двухэтапны ўваход уключаецца на bitwarden.com. Перайсці на вэб-сайт, каб зрабіць гэта?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Заблакіравана" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index e32363f0c55..091d91dc7d4 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Грешна главна парола" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Грешна главна парола. Проверете дали е-пощата е правилна и дали акаунтът Ви е създаден в $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Двустепенното вписване защищава регистрацията ви като ви кара да потвърдите влизането си чрез устройство-ключ, приложение за идентификация, мобилно съобщение, телефонно обаждане или е-поща. Двустепенното вписване може да се включи чрез сайта bitwarden.com. Искате ли да го посетите?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Ще бъдат изнесени само записите от трезора свързан с $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Само трезора свързан с $ORGANIZATION$ ще бъде експортиран. Моите записи няма да бъдат включени.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Заключено" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Показване на по-малко" }, - "enableAutotype": { - "message": "Включване на автоматичното въвеждане" - }, "enableAutotypeDescription": { "message": "Битуорден не проверява местата за въвеждане, така че се уверете, че сте в правилния прозорец, преди да ползвате клавишната комбинация." }, + "typeShortcut": { + "message": "Комбинация за въвеждане" + }, + "editAutotypeShortcutDescription": { + "message": "Използвайте един или повече от модификаторите Ctrl, Alt, Win или Shift, заедно с някоя буква." + }, + "invalidShortcut": { + "message": "Неправилна комбинация" + }, "moreBreadcrumbs": { "message": "Още елементи в пътечката", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Потвърждаване" }, - "enableAutotypeTransitionKey": { - "message": "Включване на клавишната комбинация за автоматично попълване" + "enableAutotypeShortcutPreview": { + "message": "Включване на комбинация за автоматично попълване (Функционалност в изпитание)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Уверете се, че сет в правилното поле, преди да използвате комбинацията, за да избегнете попълването на данните на грешното място." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Редактиране на комбинацията" }, - "archive": { - "message": "Архивиране" + "archiveNoun": { + "message": "Архив", + "description": "Noun" + }, + "archiveVerb": { + "message": "Архивиране", + "description": "Verb" }, "unarchive": { "message": "Изваждане от архива" diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 60b925af2e3..da0ce08dca8 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "অবৈধ মূল পাসওয়ার্ড" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "দ্বি-পদক্ষেপ লগইন অন্য ডিভাইসে আপনার লগইনটি যাচাই করার জন্য সিকিউরিটি কী, প্রমাণীকরণকারী অ্যাপ্লিকেশন, এসএমএস, ফোন কল বা ই-মেইল ব্যাবহারের মাধ্যমে আপনার অ্যাকাউন্টকে আরও সুরক্ষিত করে। bitwarden.com ওয়েব ভল্টে দ্বি-পদক্ষেপের লগইন সক্ষম করা যাবে। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index e6cdff50696..41260bf2b11 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Neispravna glavna lozinka" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Prijava u dva koraka čini Vaš račun sigurnijim tako što zahtjeva da verifikujete svoje podatke pomoću drugog uređaja, kao što su sigurnosni ključ, aplikacija za autentifikaciju, SMS, telefonski poziv ili E-Mail. Prijavljivanje u dva koraka može se omogućiti na bitwarden.com web trezoru. Da li želite da posjetite web stranicu sada?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 0defa7a878a..3f21a9d969d 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Contrasenya mestra no vàlida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a comprovar 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?" }, @@ -2371,7 +2380,7 @@ "message": "Autenticar WebAuthn" }, "readSecurityKey": { - "message": "Llegeix clau de seguretat" + "message": "Llig la clau de seguretat" }, "awaitingSecurityKeyInteraction": { "message": "S'està esperant la interacció amb la clau de seguretat..." @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Bloquejat" }, @@ -2887,7 +2914,7 @@ } }, "forwarderNoDomain": { - "message": "Domini de $SERVICENAME$ invàlid.", + "message": "Domini de $SERVICENAME$ no vàlid.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2991,7 +3018,7 @@ "message": "aplicació web" }, "notificationSentDevicePart2": { - "message": "Assegura't que la frase d'empremta digital encaixa amb la d'aquí sota abans d'aprovar-la." + "message": "Assegureu-vos que la frase de l'empremta digital coincideix amb la que hi ha a continuació abans d'aprovar-la." }, "needAnotherOptionV1": { "message": "Necessiteu una altra opció?" @@ -3108,7 +3135,7 @@ "message": "Aquesta sol·licitud ja no és vàlida." }, "confirmAccessAttempt": { - "message": "Confirma l'intent d'accés de $EMAIL$", + "message": "Confirmeu l'intent d'accés de $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -3120,7 +3147,7 @@ "message": "S'ha sol·licitat inici de sessió" }, "accountAccessRequested": { - "message": "Accés al compte demanat" + "message": "S'ha sol·licitat accés al compte" }, "creatingAccountOn": { "message": "Creant compte en" @@ -3771,7 +3798,7 @@ "message": "Denega" }, "sshkeyApprovalTitle": { - "message": "Confirma l'ús de la clau SSH" + "message": "Confirmeu l'ús de la clau SSH" }, "agentForwardingWarningTitle": { "message": "Advertència: Reenviament de l'Agent" @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 2c5ed437187..cd77dcd1d06 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Chybné hlavní heslo" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Neplatné hlavní heslo. Potvrďte správnost e-mailu a zda byl Váš účet vytvořen na $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Dvoufázové přihlášení činí Váš účet mnohem bezpečnějším díky nutnosti po každém úspěšném přihlášení zadat ověřovací kód získaný z bezpečnostního klíče, aplikace, SMS, telefonního hovoru nebo e-mailu. Dvoufázové přihlášení lze aktivovat na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Bude exportován jen trezor organizace přidružený k $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Bude exportován jen trezor organizace přidružený k $ORGANIZATION$. Položky mých sbírek nebudou zahrnuty.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Uzamčeno" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Zobrazit méně" }, - "enableAutotype": { - "message": "Povolit automatický zápis" - }, "enableAutotypeDescription": { "message": "Bitwarden neověřuje umístění vstupu. Před použitím zkratky se ujistěte, že jste ve správném okně a poli." }, + "typeShortcut": { + "message": "Napsat zkratku" + }, + "editAutotypeShortcutDescription": { + "message": "Zahrňte jeden nebo dva z následujících modifikátorů: Ctrl, Alt, Win nebo Shift a písmeno." + }, + "invalidShortcut": { + "message": "Neplatná zkratka" + }, "moreBreadcrumbs": { "message": "Více...", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Potvrdit" }, - "enableAutotypeTransitionKey": { - "message": "Povolit zkratku automatického psaní" + "enableAutotypeShortcutPreview": { + "message": "Povolit zkratku Autotype (náhled funkce)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Před použitím zkratky se ujistěte, že jste ve správném poli, abyste se vyhnuli vyplnění dat na nesprávné místo." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Upravit zkratku" }, - "archive": { - "message": "Archivovat" + "archiveNoun": { + "message": "Archiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archivovat", + "description": "Verb" }, "unarchive": { "message": "Odebrat z archivu" diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 9ff42bfa2c7..616673932ca 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 4a064a004cb..9d46c37eea2 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Ugyldig hovedadgangskode" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Totrins-login gør kontoen mere sikker ved at kræve, at man bekræfter sit login med en anden enhed, såsom en sikkerhedsnøgle, godkendelses-app, SMS, telefonopkald eller e-mail. Totrins-login kan aktiveres via bitwarden.com web-boksen. Besøg webstedet nu?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Låst" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 47b3bad34e8..56f45e04c08 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -703,10 +703,10 @@ "message": "Anhang gespeichert" }, "addAttachment": { - "message": "Add attachment" + "message": "Anhang hinzufügen" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "Die maximale Dateigröße beträgt 500 MB" }, "file": { "message": "Datei" @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Ungültiges Master-Passwort" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ungültiges Master-Passwort. Überprüfe, ob deine E-Mail-Adresse korrekt ist und dein Konto auf $HOST$ erstellt wurde.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Mit der Zwei-Faktor-Authentifizierung wird dein Konto zusätzlich abgesichert, da jede Anmeldung mit einem anderen Gerät wie einem Sicherheitsschlüssel, einer Authentifizierungs-App, einer SMS, einem Anruf oder einer E-Mail verifiziert werden muss. Die Zwei-Faktor-Authentifizierung kann im bitwarden.com Web-Tresor aktiviert werden. Möchtest du die Website jetzt öffnen?" }, @@ -1303,7 +1312,7 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Website-Symbole anzeigen und URLs zum Ändern von Passwörtern abrufen" }, "enableMinToTray": { "message": "In Infobereich-Symbol minimieren" @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Nur der mit $ORGANIZATION$ verknüpfte Organisations-Tresor wird exportiert.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Nur der mit $ORGANIZATION$ verknüpfte Organisations-Tresor wird exportiert. Meine Eintrags-Sammlungen werden nicht eingeschlossen.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Gesperrt" }, @@ -3486,10 +3513,10 @@ "message": "Sammlung auswählen" }, "importTargetHintCollection": { - "message": "Select this option if you want the imported file contents moved to a collection" + "message": "Wähle diese Option, wenn der importierte Dateiinhalt in eine Sammlung verschoben werden soll" }, "importTargetHintFolder": { - "message": "Select this option if you want the imported file contents moved to a folder" + "message": "Wähle diese Option, wenn der importierte Dateiinhalt in einen Ordner verschoben werden soll" }, "importUnassignedItemsError": { "message": "Die Datei enthält nicht zugewiesene Einträge." @@ -3586,10 +3613,10 @@ "message": "Bitte melde dich weiterhin mit deinen Firmenzugangsdaten an." }, "importDirectlyFromBrowser": { - "message": "Import directly from browser" + "message": "Direkt aus dem Browser importieren" }, "browserProfile": { - "message": "Browser Profile" + "message": "Browser-Profil" }, "seeDetailedInstructions": { "message": "Detaillierte Anleitungen auf unserer Hilfeseite unter", @@ -3834,10 +3861,10 @@ "message": "Gefährdetes Passwort ändern" }, "changeAtRiskPasswordAndAddWebsite": { - "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." + "message": "Diese Zugangsdaten sind gefährdet und es fehlt eine Website. Füge eine Website hinzu und ändere das Passwort für mehr Sicherheit." }, "missingWebsite": { - "message": "Missing website" + "message": "Fehlende Website" }, "cannotRemoveViewOnlyCollections": { "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$", @@ -3935,10 +3962,10 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "aboutThisSetting": { - "message": "About this setting" + "message": "Über diese Einstellung" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden verwendet gespeicherte URIs für die Anmeldung, um zu bestimmen, welches Symbol oder welche URL zum Ändern des Passworts verwendet werden soll, um dein Erlebnis zu verbessern. Es werden keine Informationen erfasst oder gespeichert, wenn du diesen Dienst nutzt." }, "assignToCollections": { "message": "Sammlungen zuweisen" @@ -4080,59 +4107,70 @@ "showLess": { "message": "Weniger anzeigen" }, - "enableAutotype": { - "message": "Autotype aktivieren" - }, "enableAutotypeDescription": { "message": "Bitwarden überprüft die Eingabestellen nicht. Vergewissere dich, dass du dich im richtigen Fenster und Feld befindest, bevor du die Tastenkombination verwendest." }, + "typeShortcut": { + "message": "Autotype-Tastaturkürzel" + }, + "editAutotypeShortcutDescription": { + "message": "Füge einen oder zwei der folgenden Modifikatoren ein: Strg, Alt, Win oder Umschalttaste, sowie einen Buchstaben." + }, + "invalidShortcut": { + "message": "Ungültiges Tastaturkürzel" + }, "moreBreadcrumbs": { - "message": "More breadcrumbs", + "message": "Weitere Navigationspfade", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." }, "next": { - "message": "Next" + "message": "Weiter" }, "confirmKeyConnectorDomain": { - "message": "Confirm Key Connector domain" + "message": "Key Connector-Domain bestätigen" }, "confirm": { - "message": "Confirm" + "message": "Bestätigen" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Autotype-Tastaturkürzel aktivieren (Funktionsvorschau)" }, "enableAutotypeDescriptionTransitionKey": { - "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." + "message": "Stell sicher, dass du dich im richtigen Feld befindest, bevor du das Tastaturkürzel benutzt, um zu vermeiden, dass Daten an der falschen Stelle ausgefüllt werden." }, "editShortcut": { - "message": "Edit shortcut" + "message": "Tastaturkürzel bearbeiten" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archivieren", + "description": "Verb" }, "unarchive": { - "message": "Unarchive" + "message": "Archivierung aufheben" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Einträge im Archiv" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Keine Einträge im Archiv" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen sowie Auto-Ausfüllen-Vorschlägen ausgeschlossen." }, "itemSentToArchive": { - "message": "Item sent to archive" + "message": "Eintrag an das Archiv gesendet" }, "itemRemovedFromArchive": { - "message": "Item removed from archive" + "message": "Eintrag aus dem Archiv entfernt" }, "archiveItem": { - "message": "Archive item" + "message": "Eintrag archivieren" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Archivierte Einträge werden von allgemeinen Suchergebnissen und Auto-Ausfüllen-Vorschlägen ausgeschlossen. Bist du sicher, dass du diesen Eintrag archivieren möchtest?" } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 0c2ee1fab65..da30273ec93 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Μη έγκυρος κύριος κωδικός πρόσβασης" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Η σύνδεση δύο βημάτων καθιστά τον λογαριασμό σας πιο ασφαλή απαιτώντας από εσάς να επαληθεύσετε τη σύνδεσή σας με άλλη συσκευή, όπως ένα κλειδί ασφαλείας, μία εφαρμογή αυθεντικοποίησης, ένα SMS, μία τηλεφωνική κλήση, ή ένα μήνυμα ηλ. ταχυδρομείου. Η σύνδεση δύο βημάτων μπορεί να ρυθμιστεί στη διαδικτυακή κρύπτη bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Κλειδωμένο" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 08ec76af874..4d1bf8e15fc 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4108,14 +4135,19 @@ "enableAutotypeShortcutPreview": { "message": "Enable autotype shortcut (Feature Preview)" }, - "enableAutotypeDescriptionTransitionKey": { + "enableAutotypeShortcutDescription": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." }, "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 625d8804676..22e2c375561 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 41211c2e7d7..269feba18d5 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index cebc2fa1432..540503079fa 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Ŝlosita" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 346dc0d4221..0c61da97f10 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Contraseña maestra no válida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "La autenticación en dos pasos hace que tu cuenta sea mucho más segura, requiriendo que introduzcas un código de seguridad de una aplicación de autenticación cada vez que accedes. La autenticación en dos pasos puede ser habilitada en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Bloqueado" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden no valida las ubicaciones de entrada, asegúrate de que estás en la ventana y en el capo correctos antes de usar el atajo." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index b8afeb2ed6a..a4a36241e9f 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Vale ülemparool" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Kaheastmeline kinnitamine aitab konto turvalisust tõsta. Lisaks paroolile pead kontole ligipääsemiseks kinnitama sisselogimise päringu SMS-ga, telefonikõnega, autentimise rakendusega või e-postiga. Kaheastmelist kinnitust saab sisse lülitada bitwarden.com veebihoidlas. Soovid seda kohe avada?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Lukustatud" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 7f719ec0a4b..55f02f7d7d2 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Pasahitz nagusi baliogabea" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Bi urratseko saio hasiera dela eta, zure kontua seguruagoa da, beste aplikazio/gailu batekin saioa hastea eskatzen baitizu; adibidez, segurtasun-gako, autentifikazio-aplikazio, SMS, telefono dei edo email bidez. Bi urratseko saio hasiera bitwarden.com webgunean aktibatu daiteke. Orain joan nahi duzu webgunera?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Blokeatuta" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index fbbbdfd8c7f..bb82558cc04 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "کلمه عبور اصلی نامعتبر است" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "ورود دو مرحله‌ای باعث می شود که حساب کاربری شما با استفاده از یک دستگاه دیگر مانند کلید امنیتی، برنامه احراز هویت، پیامک، تماس تلفنی و یا رایانامه، اعتبار خود را با ایمنی بیشتر اثبات کند. ورود دو مرحله‌ای می‌تواند در bitwarden.com راه‌اندازی شود. آیا می‌خواهید از سایت بازدید کنید؟" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "قفل شد" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index ecde260d80e..2d764ce69b3 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Virheellinen pääsalasana" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Kaksivaiheinen kirjautuminen parantaa tilisi suojausta vaatimalla kirjautumisen vahvistuksen salasanan lisäksi suojausavaimen, todennussovelluksen, tekstiviestin, puhelun tai sähköpostin avulla. Voit ottaa kaksivaiheisen kirjautumisen käyttöön bitwarden.com‑verkkoholvissa. Haluatko avata sen nyt?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Lukittu" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 5ad2661b46a..2ce70f049e1 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Imbalidong master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Ang two-step login ay nagpapagaan sa iyong account sa pamamagitan ng pag-verify sa iyong login sa isa pang device tulad ng security key, authenticator app, SMS, tawag sa telepono o email. Ang two-step login ay maaaring magawa sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Naka-lock" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index f85708edbe0..6c1a081926b 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Mot de passe principal invalide" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "L'authentification à deux facteurs rend votre compte plus sûr en vous demandant de vérifier votre connexion avec un autre dispositif tel qu'une clé de sécurité, une application d'authentification, un SMS, un appel téléphonique ou un courriel. L'authentification à deux facteurs peut être configurée sur le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Verrouillé" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Afficher moins" }, - "enableAutotype": { - "message": "Activer la Saisie Auto" - }, "enableAutotypeDescription": { "message": "Bitwarden ne valide pas les emplacements d'entrée, assurez-vous d'être dans la bonne fenêtre et le bon champ avant d'utiliser le raccourci." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "Plus de fil d'Ariane", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirmer" }, - "enableAutotypeTransitionKey": { - "message": "Activer le raccourci de la Saisie Auto" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Assurez-vous d'être dans le bon champ avant d'utiliser le raccourci pour éviter de remplir les données au mauvais endroit." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Modifier le raccourci" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Désarchiver" diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 5849d9d4cee..9afc8066572 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 5cbceb3ad76..fc4c4854d65 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "סיסמה ראשית שגויה" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "אימות דו שלבי הופך את החשבון שלך למאובטח יותר בכך שתצטרך לאשר התחברות בעזרת מפתח אבטחה, תוכנת אימות, SMS, שיחת טלפון, או אימייל. ניתן להפעיל את \"אימות דו שלבי\" בכספת שבאתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "נעול" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "הצג פחות" }, - "enableAutotype": { - "message": "הפעל הקלדה אוטומטית" - }, "enableAutotypeDescription": { "message": "Bitwarden לא מאמת את מקומות הקלט, נא לוודא שזה החלון והשדה הנכונים בטרם שימוש בקיצור הדרך." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "עוד סימני דרך", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "אשר" }, - "enableAutotypeTransitionKey": { - "message": "הפעל קיצור דרך להקלדה אוטומטית" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "וודא שאתה נמצא בשדה הנכון לפני השימוש בקיצור הדרך כדי להימנע ממילוי נתונים במקום הלא נכון." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "ערוך קיצור דרך" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 25ecbdf3840..f62a31c2635 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 64f89a8b15f..11ab8b7d223 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Neispravna glavna lozinka" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Nevažeća glavna lozinka. Provjeri je li tvoja adresa e-pošta ispravna i je li račun kreiran na $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Prijava dvostrukom autentifikacijom čini tvoj račun još sigurnijim tako što će zahtijevati potvrdu prijave drugim uređajem kao što je sigurnosni ključ, autentifikatorska aplikacija, SMS, poziv ili e-pošta. Prijavu dvostrukom autentifikacijom možeš omogućiti na web trezoru. Želiš li sada posjetiti bitwarden.com?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Izvezt će se samo trezor organizacije povezan s $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Izvezt će se samo trezor organizacije povezan s $ORGANIZATION$. Zbirka mojih stavki neće biti uključena.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Zaključano" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Pokaži manje" }, - "enableAutotype": { - "message": "Omogući automatski unos" - }, "enableAutotypeDescription": { "message": "Bitwarden ne provjerava lokacije unosa, prije korištenja prečaca provjeri da si u pravom prozoru i polju." }, + "typeShortcut": { + "message": "Vrsta prečaca" + }, + "editAutotypeShortcutDescription": { + "message": "Uključi jedan ili dva modifikatora: Ctrl, Alt, Win ili Shift i slovo." + }, + "invalidShortcut": { + "message": "Nevažeći prečac" + }, "moreBreadcrumbs": { "message": "Više mrvica", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Potvrdi" }, - "enableAutotypeTransitionKey": { - "message": "Omogući prečac za automatsko tipkanje" + "enableAutotypeShortcutPreview": { + "message": "Uključi prečac Autotype (Pretpregled značajke)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Prije korištenja prečaca provjeri nalaziš li se u ispravnom polju kako se podaci ne bi unijeli na pogrešno mjesto." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Uredi prečac" }, - "archive": { - "message": "Arhiviraj" + "archiveNoun": { + "message": "Arhiva", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arhiviraj", + "description": "Verb" }, "unarchive": { "message": "Poništi arhiviranje" diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 9f71448ce5a..393340a6dfb 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "A mesterjelszó érvénytelen." }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "A mesterjelszó érvénytelen. Erősítsük meg, hogy email cím helyes és a fiók létrehozásának helye: $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "A kétlépcsős bejelentkezés biztonságosabbá teszi a fiókot azzal, hogy meg kell erősíteni a bejelentkezést egy másik olyan eszközzel mint például biztonsági kulcs, hitelesítő alkalmazás, SMS, telefonhívás vagy email. A kétlépcsős bejelentkezést a bitwarden.com webes széfben lehet megváltoztatni. Szeretnénk felkeresni most a webhelyet?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Csak a $ORGANIZATION$ szervezetehez kapcsolódó szervezeti széf kerül exportálásra.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Csak a $ORGANIZATION$ szervezethez kapcsolódó szervezeti széf kerül exportálásra. A saját elem gyűjtemények nem lesznek benne.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Lezárva" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Kevesebb megjelenítése" }, - "enableAutotype": { - "message": "Autotípus engedélyezése" - }, "enableAutotypeDescription": { "message": "A Bitwarden nem érvényesíti a beviteli helyeket, győződjünk meg róla, hogy a megfelelő ablakban és mezőben vagyunk, mielőtt a parancsikont használnánk." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "További morzsamenük", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Megerősítés" }, - "enableAutotypeTransitionKey": { - "message": "Automatikus típusú parancsikon engedélyezése" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Győződjünk meg arról, hogy a megfelelő mezőben vagyunk, mielőtt a parancsikont használnánk, hogy elkerüljük az adatok rossz helyre történő kitöltését." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Parancsikon szerkesztése" }, - "archive": { - "message": "Archívum" + "archiveNoun": { + "message": "Archívum", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archívum", + "description": "Verb" }, "unarchive": { "message": "Visszavétel archívumból" diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 3f44fd8bf97..445f999ca6d 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Sandi utama tidak valid" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Info masuk dua langkah membuat akun Anda lebih aman dengan mengharuskan Anda memverifikasi info masuk Anda dengan peranti lain seperti kode keamanan, aplikasi autentikasi, SMS, panggilan telepon, atau email. Info masuk dua langkah dapat diaktifkan di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Terkunci" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 780d09f3582..97a00443f4d 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Password principale errata" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Bloccato" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Mostra di meno" }, - "enableAutotype": { - "message": "Abilita auto immissione" - }, "enableAutotypeDescription": { "message": "Bitwarden non convalida i campi di input: assicurati di essere nella finestra e nel campo di testo corretti prima di usare la scorciatoia." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "Ulteriori segmenti", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Conferma" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index a58543302fa..7c1cd89dd61 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "マスターパスワードが間違っています" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "2段階認証を使うと、ログイン時にセキュリティキーや認証アプリ、SMS、電話やメールでの認証を必要にすることでアカウントをさらに安全に出来ます。2段階認証は bitwarden.com ウェブ保管庫で有効化できます。ウェブサイトを開きますか?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "ロック中" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 0bb7e929979..4095eccade0 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -262,7 +262,7 @@ "message": "Remember until vault is locked" }, "premiumRequired": { - "message": "Premium required" + "message": "საჭიროა პრემიუმი" }, "premiumRequiredDesc": { "message": "A Premium membership is required to use this feature." @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1601,7 +1610,7 @@ "message": "ყველაფრის ჩვენება" }, "quitBitwarden": { - "message": "Quit Bitwarden" + "message": "გამოდით Bitwarden-იდან" }, "valueCopied": { "message": "$VALUE$ copied", @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "დაბლოკილია" }, @@ -3852,7 +3879,7 @@ "message": "Move" }, "newFolder": { - "message": "New folder" + "message": "ახალი საქაღალდე" }, "folderName": { "message": "Folder Name" @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 5849d9d4cee..9afc8066572 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 66a6e43d0cb..54909553293 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "ಅಮಾನ್ಯ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "ಭದ್ರತಾ ಕೀ, ದೃಢೀಕರಣ ಅಪ್ಲಿಕೇಶನ್, ಎಸ್‌ಎಂಎಸ್, ಫೋನ್ ಕರೆ ಅಥವಾ ಇಮೇಲ್‌ನಂತಹ ಮತ್ತೊಂದು ಸಾಧನದೊಂದಿಗೆ ನಿಮ್ಮ ಲಾಗಿನ್ ಅನ್ನು ಪರಿಶೀಲಿಸುವ ಅಗತ್ಯವಿರುವ ಎರಡು ಹಂತದ ಲಾಗಿನ್ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಹೆಚ್ಚು ಸುರಕ್ಷಿತಗೊಳಿಸುತ್ತದೆ. ಬಿಟ್ವಾರ್ಡೆನ್.ಕಾಮ್ ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 59423b8ad73..f210b5b5cae 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "잘못된 마스터 비밀번호" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "2단계 인증은 보안 키, 인증 앱, SMS, 전화 통화 등의 다른 기기로 사용자의 로그인 시도를 검증하여 사용자의 계정을 더욱 안전하게 만듭니다. 2단계 인증은 bitwarden.com 웹 보관함에서 활성화할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "잠김" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 8159bc5e28b..e03c9dcd0f8 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Neteisingas pagrindinis slaptažodis" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Prisijungus dviem veiksmais, jūsų paskyra tampa saugesnė, reikalaujant patvirtinti prisijungimą naudojant kitą įrenginį, pvz., Saugos raktą, autentifikavimo programą, SMS, telefono skambutį ar el. Paštą. Dviejų žingsnių prisijungimą galima įjungti „bitwarden.com“ interneto saugykloje. Ar norite dabar apsilankyti svetainėje?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Užrakinta" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 5e29f10190b..4ad26552858 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Nederīga galvenā parole" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Nederīga galvenā parole. Jāpārliecinās, ka e-pasta adrese ir pareiza un konts tika izveidots $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Divpakāpju pieteikšanās padara kontu krietni drošāku, pieprasot apstiprināt pieteikšanos ar tādu citu ierīču vai pakalpojumu starpniecību kā drošības atslēga, autentificētāja lietotne, īsziņa, tālruņa zvans vai e-pasts. Divpakāpju pieteikšanos var iespējot bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Aizslēgta" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Rādīt mazāk" }, - "enableAutotype": { - "message": "Iespējot automātisko ievadi" - }, "enableAutotypeDescription": { "message": "Bitwarden nepārbauda ievades atrašanās vietas, jāpārliecinās, ka atrodies pareizajā logā un laukā, pirms saīsnes izmantošanas." }, + "typeShortcut": { + "message": "Ievadīt īsinājumtaustiņus" + }, + "editAutotypeShortcutDescription": { + "message": "Jāiekļauj viens vai divi no šiem taustiņiem - Ctrl, Alt, Win vai Shift - un burts." + }, + "invalidShortcut": { + "message": "Nederīgi īsinājumtaustiņi" + }, "moreBreadcrumbs": { "message": "Vairāk norāžu", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Apstiprināt" }, - "enableAutotypeTransitionKey": { - "message": "Iespējot automātiskās ievades saīsni" + "enableAutotypeShortcutPreview": { + "message": "Iespējot automātiskās ievades īsinājumtaustiņus (iespējas priekšskatījums)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Jāpārliecinās, ka pirms saīsnes izmantošanas kursors ir pareizajā laukā, lai izvairītos no datu ievadīšanas nepareizā vietā." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Labot saīsni" }, - "archive": { - "message": "Arhivēt" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Atcelt arhivēšanu" diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index b023d0efab0..6f6086ebc57 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Nevažeća glavna lozinka" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Prijavljivanje u dva koraka čini vaš nalog sigurnijim tako što ćete morati da verifikujete prijavu na drugom uređaju, kao što su bezbjedonosni ključ, aplikacija za potvrđivanje, SMS, telefonski poziv ili e-pošta. Prijava u dva koraka može se omogućiti u trezoru na internet strani bitwarden.com. Da li želite da posjetite internet lokaciju sada?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 863f3941a0f..4bc82a9a543 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "അസാധുവായ പ്രാഥമിക പാസ്‌വേഡ്" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "സുരക്ഷാ കീ, ഓതന്റിക്കേറ്റർ അപ്ലിക്കേഷൻ, SMS, ഫോൺ കോൾ അല്ലെങ്കിൽ ഇമെയിൽ പോലുള്ള മറ്റൊരു ഉപകരണം ഉപയോഗിച്ച് തങ്ങളുടെ ലോഗിൻ സ്ഥിരീകരിക്കാൻ ആവശ്യപ്പെടുന്നതിലൂടെ രണ്ട്-ഘട്ട ലോഗിൻ തങ്ങളുടെ അക്കൗണ്ടിനെ കൂടുതൽ സുരക്ഷിതമാക്കുന്നു. bitwarden.com വെബ് വാൾട്ടിൽ രണ്ട്-ഘട്ട ലോഗിൻ പ്രവർത്തനക്ഷമമാക്കാനാകും.തങ്ങള്ക്കു ഇപ്പോൾ വെബ്സൈറ്റ് സന്ദർശിക്കാൻ ആഗ്രഹമുണ്ടോ?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 5849d9d4cee..9afc8066572 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index fae67d310f1..cb01bd340b5 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index ec1c1bdb9b5..3e7d5a646eb 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Ugyldig hovedpassord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "2-trinnsinnlogging gjør kontoen din mer sikker, ved å kreve at du verifiserer din innlogging med en annen enhet, f.eks. en autentiseringsapp, SMS, E-post, telefonsamtale, eller sikkerhetsnøkkel. 2-trinnsinnlogging kan aktiveres på bitwarden.com-netthvelvet. Vil du besøke den nettsiden nå?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Låst" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 813fa967252..12edd751bff 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index c726c003776..a3896955162 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Ongeldig hoofdwachtwoord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ongeldig hoofdwachtwoord. Check of je e-mailadres klopt en of je account is aangemaakt op $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Tweestapsaanmelding beschermt je account door je inlogpoging te bevestigen met een ander apparaat zoals een beveiligingssleutel, authenticatie-app, SMS, spraakoproep of e-mail. Je kunt Tweestapsaanmelding inschakelen in de webkluis op bitwarden.com. Wil je de website nu bezoeken?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Alleen de organisatiekluis die gekoppeld is aan $ORGANIZATION$ wordt geëxporteerd.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Exporteert alleen de organisatiekluis van $ORGANIZATION$. Geen persoonlijke kluis-items.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Vergrendeld" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Minder weergeven" }, - "enableAutotype": { - "message": "Autotypen inschakelen" - }, "enableAutotypeDescription": { "message": "Bitwarden valideert de invoerlocaties niet, zorg ervoor dat je je in het juiste venster en veld bevindt voordat je de snelkoppeling gebruikt." }, + "typeShortcut": { + "message": "Typ de sneltoets" + }, + "editAutotypeShortcutDescription": { + "message": "Voeg een of twee van de volgende toetsen toe: Ctrl, Alt, Win of Shift, en een letter." + }, + "invalidShortcut": { + "message": "Ongeldige sneltoets" + }, "moreBreadcrumbs": { "message": "Meer broodkruimels", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Bevestigen" }, - "enableAutotypeTransitionKey": { - "message": "Snelkoppeling autotype inschakelen" + "enableAutotypeShortcutPreview": { + "message": "Autotype sneltoets inschakelen (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Zorg ervoor dat je je in het juiste veld bevindt voordat je de snelkoppeling gebruikt om te voorkomen dat je de gegevens op de verkeerde plaats invult." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Snelkoppeling bewerken" }, - "archive": { - "message": "Archiveren" + "archiveNoun": { + "message": "Archief", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archiveren", + "description": "Verb" }, "unarchive": { "message": "Dearchiveren" diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 94c2196edfa..f33379eed7b 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Ugyldig hovudpassord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 217439bec80..54971e9edc0 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 6caee67447f..243cae0be72 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Hasło główne jest nieprawidłowe" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Logowanie dwustopniowe zwiększa bezpieczeństwo konta, wymagając weryfikacji logowania za pomocą innego urządzenia, takiego jak klucz bezpieczeństwa, aplikacja uwierzytelniająca, wiadomość SMS, połączenie telefoniczne lub wiadomość e-mail. Logowanie dwustopniowe możesz skonfigurować w sejfie internetowym bitwarden.com. Czy chcesz przejść do strony?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Zablokowane" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Pokaż mniej" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "Więcej nawigacji", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Potwierdź" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edytuj skrót" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archiwum", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archiwizuj", + "description": "Verb" }, "unarchive": { "message": "Usuń z archiwum" @@ -4133,6 +4171,6 @@ "message": "Archiwizuj element" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Zarchiwizowane elementy są wykluczone z wyników wyszukiwania i sugestii autouzupełniania. Czy na pewno chcesz archiwizować element?" } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 4b28ca4918a..0731e7f6bc8 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Senha mestra inválida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "A autenticação em duas etapas torna sua conta mais segura, exigindo que você verifique o seu login com outro dispositivo, como uma chave de segurança, um aplicativo de autenticação, SMS, chamada telefônica ou e-mail. A autenticação em duas etapas pode ser ativada no cofre web em bitwarden.com. Você deseja visitar o site agora?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Bloqueado" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Mostrar menos" }, - "enableAutotype": { - "message": "Habilitar Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden não valida localizações de entrada, tenha certeza de estar na janela e campo corretos antes de utilizar o atalho." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "Mais trilhas", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 49573dcd647..74c4e3077d2 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Palavra-passe mestra inválida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Palavra-passe mestra inválida. Confirme se o seu e-mail está correto e se a sua conta foi criada em $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "A verificação de dois passos torna a sua conta mais segura, exigindo que verifique o seu início de sessão com outro dispositivo, como uma chave de segurança, aplicação de autenticação, SMS, chamada telefónica ou e-mail. A verificação de dois passos pode ser configurada em bitwarden.com. Pretende visitar o site agora?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Apenas o cofre da organização associado a $ORGANIZATION$ será exportado.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Apenas o cofre da organização associado a $ORGANIZATION$ será exportado. As coleções dos meus itens não serão incluídas.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Bloqueado" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Mostrar menos" }, - "enableAutotype": { - "message": "Ativar digitação automática" - }, "enableAutotypeDescription": { "message": "O Bitwarden não valida a introdução de localizações. Certifique-se de que está na janela e no campo corretos antes de utilizar o atalho." }, + "typeShortcut": { + "message": "Introduzir atalho" + }, + "editAutotypeShortcutDescription": { + "message": "Inclua um ou dois dos seguintes modificadores: Ctrl, Alt, Win, ou Shift, e uma letra." + }, + "invalidShortcut": { + "message": "Atalho inválido" + }, "moreBreadcrumbs": { "message": "Mais da navegação estrutural", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirmar" }, - "enableAutotypeTransitionKey": { - "message": "Ativar o atalho de introdução automática" + "enableAutotypeShortcutPreview": { + "message": "Ativar o atalho de digitação automática (Pré-visualização da funcionalidade)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Certifique-se de que está no campo correto antes de utilizar o atalho para evitar preencher dados no local errado." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Editar atalho" }, - "archive": { - "message": "Arquivar" + "archiveNoun": { + "message": "Arquivo", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arquivar", + "description": "Verb" }, "unarchive": { "message": "Desarquivar" diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 802afc3ef22..5d72560fabb 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Parolă principală incorectă" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Autentificarea în două etape vă face contul mai sigur, cerându-vă să vă verificați autentificarea cu un alt dispozitiv, cum ar fi o cheie de securitate, o aplicație de autentificare, un SMS, un apel telefonic sau un e-mail. Autentificarea în două etape poate fi configurată pe seiful web bitwarden.com. Doriți să vizitați site-ul web acum?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Blocat" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 292564be5f3..c1c2cee8bf5 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Неверный мастер-пароль" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Неверный мастер-пароль. Подтвердите, что ваш адрес email указан верно и ваш аккаунт был создан на $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Двухэтапная аутентификация делает аккаунт более защищенным, поскольку требуется подтверждение входа при помощи другого устройства, например, ключа безопасности, приложения-аутентификатора, SMS, телефонного звонка или электронной почты. Двухэтапная аутентификация включается на bitwarden.com. Перейти на сайт сейчас?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Будет экспортировано только хранилище организации, связанное с $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Будет экспортировано только хранилище организации, связанное с $ORGANIZATION$. Коллекции Мои элементы включены не будут.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Заблокировано" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Меньше" }, - "enableAutotype": { - "message": "Включить автоввод" - }, "enableAutotypeDescription": { "message": "Bitwarden не проверяет местоположение ввода, поэтому, прежде чем использовать ярлык, убедитесь, что вы находитесь в нужном окне и поле." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "Больше хлебных крошек", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Подтвердить" }, - "enableAutotypeTransitionKey": { - "message": "Включить ярлык автоввода" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Прежде чем использовать ярлык, убедитесь, что вы поставили курсор в нужное поле, чтобы избежать ввода данных в неправильное место." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Изменить ярлык" }, - "archive": { - "message": "Архив" + "archiveNoun": { + "message": "Архив", + "description": "Noun" + }, + "archiveVerb": { + "message": "Архивировать", + "description": "Verb" }, "unarchive": { "message": "Разархивировать" diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 8d4072e1da2..65cd68c9d53 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 566b9b8210a..491d91913f9 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Neplatné hlavné heslo" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Neplatné hlavné heslo. Potvrďte, že váš e-mail je správny a účet bol vytvorený na $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Dvojstupňové prihlasovanie robí váš účet bezpečnejším vďaka vyžadovaniu bezpečnostného kódu z overovacej aplikácie vždy, keď sa prihlásite. Dvojstupňové prihlasovanie môžete povoliť vo webovom trezore bitwarden.com. Chcete teraz navštíviť túto stránku?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Exportuje sa len trezor organizácie spojený s $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Exportuje sa len trezor organizácie spojený s $ORGANIZATION$. Moje zbierky položiek nebudú zahrnuté.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Zamknutý" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Zobraziť menej" }, - "enableAutotype": { - "message": "Povoliť automatické vpisovanie" - }, "enableAutotypeDescription": { "message": "Bitwarden neoveruje miesto stupu, pred použitím skratky sa uistite, že ste v správnom okne a poli." }, + "typeShortcut": { + "message": "Zadajte klávesovú skratku" + }, + "editAutotypeShortcutDescription": { + "message": "Použite jeden alebo dva z nasledujúcich modifikátorov: Ctrl, Alt, Win, alebo Shift a písmeno." + }, + "invalidShortcut": { + "message": "Neplatná klávesová skratka" + }, "moreBreadcrumbs": { "message": "Viac", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Potvrdiť" }, - "enableAutotypeTransitionKey": { - "message": "Povoliť skratku automatického písania" + "enableAutotypeShortcutPreview": { + "message": "Povoliť skratku pre automatické vpisovanie (náhľad funkcie)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Pred použitím skratky sa uistite, že sa nachádzate v správnom poli, aby ste údaje nevyplnili na nesprávne miesto." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Upraviť skratku" }, - "archive": { - "message": "Archivovať" + "archiveNoun": { + "message": "Archív", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archivovať", + "description": "Verb" }, "unarchive": { "message": "Zrušiť archiváciu" diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 23a6df7ad95..dc1585c2431 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Napačno glavno geslo" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Avtentikacija v dveh korakih naredi vaš račun bolj varen, saj od vas zahteva, da svojo prijavo preverite z drugo napravo, kot je varnostni ključ, aplikacija za preverjanje pristnosti, SMS, telefonski klic ali e-pošta. V spletnem trezorju bitwarden.com je lahko omogočite prijavo v dveh korakih. Ali želite spletno stran obiskati sedaj?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index cd41aa9e4b9..8e68918ff5b 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Погрешна главна лозинка" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Неисправна главна лозинка. Потврдите да је адреса ваше е-поште исправна и да је ваш налог направљен на $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Пријава у два корака чини ваш налог сигурнијим захтевом да верификујете своје податке помоћу другог уређаја, као што су безбедносни кључ, апликација, СМС-а, телефонски позив или имејл. Пријављивање у два корака може се омогућити на веб сефу. Да ли желите да посетите веб страницу сада?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Извешће се само сеф организације повезана са $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Извешће се само сеф организације повезан са $ORGANIZATION$. Колекције мојих предмета неће бити укључене.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Закључано" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Прикажи мање" }, - "enableAutotype": { - "message": "Упали ауто-унос" - }, "enableAutotypeDescription": { "message": "Bitwarden не потврђује локације уноса, будите сигурни да сте у добром прозору и поље пре употребе пречице." }, + "typeShortcut": { + "message": "Унети пречицу" + }, + "editAutotypeShortcutDescription": { + "message": "Укључите један или два следећа модификатора: Ctrl, Alt, Win, или Shift, и слово." + }, + "invalidShortcut": { + "message": "Неважећа пречица" + }, "moreBreadcrumbs": { "message": "Више мрвица", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Потврди" }, - "enableAutotypeTransitionKey": { - "message": "Омогућава пречицу за аутоматски унос" + "enableAutotypeShortcutPreview": { + "message": "Омогућите пречицу ауто-уноса (преглед функције)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Будите сигурни да сте у исправном пољу пре употребе пречице да бисте избегли попуњавање података на погрешно место." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Уреди пречицу" }, - "archive": { - "message": "Архива" + "archiveNoun": { + "message": "Архива", + "description": "Noun" + }, + "archiveVerb": { + "message": "Архивирај", + "description": "Verb" }, "unarchive": { "message": "Врати из архиве" diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index a867fc28753..45caa056055 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Ogiltigt huvudlösenord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ogiltigt huvudlösenord. Bekräfta att din e-postadress är korrekt och ditt konto skapades på $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Tvåstegsverifiering gör ditt konto säkrare genom att kräva att du verifierar din inloggning med en annan enhet, t.ex. en säkerhetsnyckel, autentiseringsapp, SMS, telefonsamtal eller e-post. Tvåstegsverifiering kan aktiveras i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Endast organisationsvalvet som är associerat med $ORGANIZATION$ kommer att exporteras.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Endast organisationsvalvet som associeras med $ORGANIZATION$ kommer att exporteras. Mina objektsamlingar kommer inte att inkluderas.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Låst" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Visa mindre" }, - "enableAutotype": { - "message": "Aktivera automatisk inmatning" - }, "enableAutotypeDescription": { "message": "Bitwarden validerar inte inmatningsplatser, så se till att du är i rätt fönster och fält innan du använder genvägen." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Inkludera en eller två av följande modifierare: Ctrl, Alt, Win, eller Skift och en bokstav." + }, + "invalidShortcut": { + "message": "Ogiltig genväg" + }, "moreBreadcrumbs": { "message": "Fler länkstigar", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Bekräfta" }, - "enableAutotypeTransitionKey": { - "message": "Aktivera genväg för automatisk inmatning" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Försäkra dig om att du befinner dig i rätt fält innan du använder genvägen för att undvika att fylla i data på fel ställe." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Redigera genväg" }, - "archive": { - "message": "Arkivera" + "archiveNoun": { + "message": "Arkiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arkivera", + "description": "Verb" }, "unarchive": { "message": "Packa upp" diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index 4874985a8fd..1fb449c891f 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "தவறான முதன்மை கடவுச்சொல்" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "இரண்டு-படி உள்நுழைவு உங்கள் கணக்கை ஒரு பாதுகாப்பு விசை, அங்கீகரிப்பான் செயலி, SMS, ஃபோன் அழைப்பு அல்லது மின்னஞ்சல் போன்ற மற்றொரு சாதனம் மூலம் உங்கள் உள்நுழைவை சரிபார்க்க கோருவதன் மூலம் அதை மேலும் பாதுகாக்கிறது. bitwarden.com இணைய பெட்டகத்தில் இரண்டு-படி உள்நுழைவை அமைக்கலாம். இப்போது இணையதளத்திற்குச் செல்ல விரும்புகிறீர்களா?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "பூட்டப்பட்டது" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "குறைவாகக் காட்டு" }, - "enableAutotype": { - "message": "தானியங்கு வகையை இயக்கு" - }, "enableAutotypeDescription": { "message": "Bitwarden உள்ளீட்டு இடங்களைச் சரிபார்க்காது, ஷார்ட்கட்டைப் பயன்படுத்துவதற்கு முன் சரியான சாளரம் மற்றும் புலத்தில் நீங்கள் இருக்கிறீர்கள் என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "மேலும் பிரெட்க்ரம்புகள்", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "உறுதிப்படுத்து" }, - "enableAutotypeTransitionKey": { - "message": "தானியங்கு வகை குறுக்குவழியை இயக்கு" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "தவறான இடத்தில் தரவை நிரப்புவதைத் தவிர்க்க, குறுக்குவழியைப் பயன்படுத்துவதற்கு முன்பு நீங்கள் சரியான புலத்தில் இருப்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "குறுக்குவழியைத் திருத்து" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 5849d9d4cee..9afc8066572 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index fa619695fdb..1a66578c55c 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "รหัสผ่านหลักไม่ถูกต้อง" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "การเข้าสู่ระบบแบบสองขั้นตอนทำให้บัญชีของคุณมีความปลอดภัยมากขึ้นด้วยการให้คุณตรวจสอบการเข้าสู่ระบบของคุณกับอุปกรณ์อื่นเช่นคีย์ความปลอดภัย, แอพ authenticator, SMS, โทรศัพท์หรืออีเมล. เข้าสู่ระบบแบบสองขั้นตอนสามารถเปิดใช้งานบน เว็บนิรภัย bitwarden.com คุณต้องการเยี่ยมชมเว็บไซต์เดี๋ยวนี้หรือไม่" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Locked" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index c33570af387..542e5b07ace 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Geçersiz ana parola" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ana parola geçersiz. E-posta adresinizin doğru olduğunu ve hesabınızın $HOST$ üzerinde oluşturulduğunu kontrol edin.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "İki aşamalı giriş, hesabınıza girererken işlemi bir güvenlik anahtarı, şifrematik uygulaması, SMS, telefon araması veya e-posta gibi ek bir yöntemle doğrulamanızı isteyerek hesabınızın güvenliğini artırır. İki aşamalı giriş özelliğini bitwarden.com web kasası üzerinden ayarlayabilirsiniz. Şimdi siteye gitmek ister misiniz?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Kilitli" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Daha az göster" }, - "enableAutotype": { - "message": "Otomatik yazmayı etkinleştir" - }, "enableAutotypeDescription": { "message": "Bitwarden giriş konumlarını doğrulamaz, kısayolu kullanmadan önce doğru pencerede ve alanda olduğunuzdan emin olun." }, + "typeShortcut": { + "message": "Kısayolu yazın" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Geçersiz kısayol" + }, "moreBreadcrumbs": { "message": "Daha fazla gezinme izi", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Onayla" }, - "enableAutotypeTransitionKey": { - "message": "Otomatik yazma kısayolunu etkinleştir" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Verilerin yanlış yere doldurulmasını önlemek için kısayolu kullanmadan önce doğru alanda olduğunuzdan emin olun." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Kısayolu düzenle" }, - "archive": { - "message": "Arşivle" + "archiveNoun": { + "message": "Arşiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arşivle", + "description": "Verb" }, "unarchive": { "message": "Arşivden çıkar" diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index c5c86baacdf..3a38ad2666e 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Неправильний головний пароль" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Двоетапна перевірка дає змогу надійніше захистити ваш обліковий запис, вимагаючи підтвердження входу з використанням іншого пристрою, наприклад, за допомогою ключа безпеки, програми автентифікації, SMS, телефонного виклику, або е-пошти. Ви можете налаштувати двоетапну перевірку в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Заблоковано" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Згорнути" }, - "enableAutotype": { - "message": "Увімкнути автовведення" - }, "enableAutotypeDescription": { "message": "Bitwarden не перевіряє місця введення. Переконайтеся, що у вас відкрите правильне вікно і вибрано потрібне поле, перш ніж застосувати комбінацію клавіш." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "Інші елементи", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index a9ac6aa5bd7..c044d36b47f 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "Mật khẩu chính không hợp lệ" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Mật khẩu chính không hợp lệ. Xác nhận email của bạn là chính xác và tài khoản được tạo trên $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "Đăng nhập hai bước giúp tài khoản của bạn an toàn hơn bằng cách yêu cầu bạn xác minh việc đăng nhập bằng một thiết bị khác như khóa bảo mật, ứng dụng xác thực, SMS, cuộc gọi điện thoại hoặc email. Đăng nhập hai bước có thể được thiết lập trên bitwarden.com. Bạn có muốn truy cập trang web bây giờ không?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Chỉ kho lưu trữ tổ chức liên kết với $ORGANIZATION$ sẽ được xuất.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Chỉ kho lưu trữ tổ chức liên kết với $ORGANIZATION$ được xuất. Bộ sưu tập mục của tôi sẽ không được bao gồm.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "Đã khóa" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "Thu gọn" }, - "enableAutotype": { - "message": "Bật tính năng Tự động nhập liệu" - }, "enableAutotypeDescription": { "message": "Bitwarden không kiểm tra vị trí nhập liệu, hãy đảm bảo bạn đang ở trong đúng cửa sổ và trường nhập liệu trước khi dùng phím tắt." }, + "typeShortcut": { + "message": "Phím tắt nhập liệu" + }, + "editAutotypeShortcutDescription": { + "message": "Bao gồm một hoặc hai trong số các phím bổ trợ sau: Ctrl, Alt, Win hoặc Shift, và một chữ cái." + }, + "invalidShortcut": { + "message": "Phím tắt không hợp lệ" + }, "moreBreadcrumbs": { "message": "Thêm mục điều hướng", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Xác nhận" }, - "enableAutotypeTransitionKey": { - "message": "Bật phím tắt tự động điền" + "enableAutotypeShortcutPreview": { + "message": "Bật phím tắt tự động nhập (Xem trước tính năng)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Hãy đảm bảo bạn đang ở đúng trường trước khi sử dụng phím tắt để tránh điền dữ liệu vào chỗ không đúng." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Chỉnh sửa phím tắt" }, - "archive": { - "message": "Lưu trữ" + "archiveNoun": { + "message": "Lưu trữ", + "description": "Noun" + }, + "archiveVerb": { + "message": "Lưu trữ", + "description": "Verb" }, "unarchive": { "message": "Hủy lưu trữ" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 552afdca34c..974e3d39101 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -576,7 +576,7 @@ "message": "复制验证码 (TOTP)" }, "copyFieldCipherName": { - "message": "复制 $CIPHERNAME$ 的 $FIELD$", + "message": "复制 $CIPHERNAME$ 中的 $FIELD$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "无效的主密码" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "无效的主密码。请确认您的电子邮箱正确无误,以及您的账户是在 $HOST$ 上创建的。", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "两步登录要求您从其他设备(例如安全密钥、验证器 App、短信、电话或者电子邮件)来验证您的登录,这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库。", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库,不包括我的项目集合。", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "已锁定" }, @@ -4080,12 +4107,18 @@ "showLess": { "message": "显示更少" }, - "enableAutotype": { - "message": "启用自动输入" - }, "enableAutotypeDescription": { "message": "Bitwarden 不会验证输入位置,在使用快捷键之前,请确保您位于正确的窗口和字段中。" }, + "typeShortcut": { + "message": "输入快捷键" + }, + "editAutotypeShortcutDescription": { + "message": "包括以下一个或两个修饰符:Ctrl、Alt、Win 或 Shift,外加一个字母。" + }, + "invalidShortcut": { + "message": "无效的快捷键" + }, "moreBreadcrumbs": { "message": "更多导航项", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "确认" }, - "enableAutotypeTransitionKey": { - "message": "启用自动输入快捷键" + "enableAutotypeShortcutPreview": { + "message": "启用自动输入快捷键(功能预览)" }, "enableAutotypeDescriptionTransitionKey": { "message": "在使用快捷键之前,请确保您位于正确的字段中,以避免将数据填入错误的地方。" @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "编辑快捷键" }, - "archive": { - "message": "归档" + "archiveNoun": { + "message": "归档", + "description": "Noun" + }, + "archiveVerb": { + "message": "归档", + "description": "Verb" }, "unarchive": { "message": "取消归档" diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index d4e579f89c1..60bf5cae265 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -1222,6 +1222,15 @@ "invalidMasterPassword": { "message": "無效的主密碼" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "twoStepLoginConfirmation": { "message": "兩步驟登入需要您從其他裝置(例如安全鑰匙、驗證器程式、SMS、手機或電子郵件)來驗證您的登入,這使您的帳戶更加安全。兩步驟登入可以在 bitwarden.com 網頁版密碼庫啟用。現在要前往嗎?" }, @@ -2654,6 +2663,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "locked": { "message": "已鎖定" }, @@ -3075,7 +3102,7 @@ "message": "Login request" }, "deviceType": { - "message": "裝置類別" + "message": "裝置類型" }, "ipAddress": { "message": "IP 位址" @@ -4080,12 +4107,18 @@ "showLess": { "message": "Show less" }, - "enableAutotype": { - "message": "Enable Autotype" - }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + }, + "invalidShortcut": { + "message": "Invalid shortcut" + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." @@ -4099,8 +4132,8 @@ "confirm": { "message": "Confirm" }, - "enableAutotypeTransitionKey": { - "message": "Enable autotype shortcut" + "enableAutotypeShortcutPreview": { + "message": "Enable autotype shortcut (Feature Preview)" }, "enableAutotypeDescriptionTransitionKey": { "message": "Be sure you are in the correct field before using the shortcut to avoid filling data into the wrong place." @@ -4108,8 +4141,13 @@ "editShortcut": { "message": "Edit shortcut" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "unarchive": { "message": "Unarchive" diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 1595252251b..993084f7724 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -500,9 +500,9 @@ export class WindowMain { displayBounds.x !== state.displayBounds.x || displayBounds.y !== state.displayBounds.y ) { - state.x = undefined; - state.y = undefined; displayBounds = screen.getPrimaryDisplay().bounds; + state.x = displayBounds.x + displayBounds.width / 2 - state.width / 2; + state.y = displayBounds.y + displayBounds.height / 2 - state.height / 2; } } diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 706ad67aa2a..8db495f69c8 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.9.1", + "version": "2025.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.9.1", + "version": "2025.10.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 7d8d0cc18a2..c4b4992e754 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2025.9.1", + "version": "2025.10.0", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/platform/services/electron-storage.service.ts b/apps/desktop/src/platform/services/electron-storage.service.ts index 2d292d6537b..34aa8837475 100644 --- a/apps/desktop/src/platform/services/electron-storage.service.ts +++ b/apps/desktop/src/platform/services/electron-storage.service.ts @@ -3,6 +3,7 @@ import * as fs from "fs"; import { ipcMain } from "electron"; +import ElectronStore from "electron-store"; import { Subject } from "rxjs"; import { @@ -11,22 +12,7 @@ import { } from "@bitwarden/common/platform/abstractions/storage.service"; import { NodeUtils } from "@bitwarden/node/node-utils"; -// See: https://github.com/sindresorhus/electron-store/blob/main/index.d.ts -interface ElectronStoreOptions { - defaults: unknown; - name: string; -} - -type ElectronStoreConstructor = new (options: ElectronStoreOptions) => ElectronStore; - -// eslint-disable-next-line -const Store: ElectronStoreConstructor = require("electron-store"); - -interface ElectronStore { - get: (key: string) => unknown; - set: (key: string, obj: unknown) => void; - delete: (key: string) => void; -} +import { isWindowsPortable } from "../../utils"; interface BaseOptions { action: T; @@ -48,11 +34,13 @@ export class ElectronStorageService implements AbstractStorageService { if (!fs.existsSync(dir)) { NodeUtils.mkdirpSync(dir, "700"); } - const storeConfig: ElectronStoreOptions = { + const fileMode = isWindowsPortable() ? 0o666 : 0o600; + const storeConfig: ElectronStore.Options> = { defaults: defaults, name: "data", + configFileMode: fileMode, }; - this.store = new Store(storeConfig); + this.store = new ElectronStore(storeConfig); this.updates$ = this.updatesSubject.asObservable(); ipcMain.handle("storageService", (event, options: Options) => { diff --git a/apps/desktop/src/scss/misc.scss b/apps/desktop/src/scss/misc.scss index b64bdd92120..c70eb823213 100644 --- a/apps/desktop/src/scss/misc.scss +++ b/apps/desktop/src/scss/misc.scss @@ -360,11 +360,16 @@ form, } } -.settings-link { +.help-block a.settings-link { + text-decoration: none; @include themify($themes) { color: themed("primaryColor"); + + &:hover, + &:focus { + color: darken(themed("primaryColor"), 6%); + } } - font-weight: bold; } app-root > #loading, diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html index e123f4400c7..f83a2e1c91f 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html @@ -55,7 +55,10 @@ >  {{ c.node.name }} diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 3fdb14aa154..5f888e081c1 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -45,6 +45,7 @@ import { CipherViewLike, CipherViewLikeUtils, } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; +import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { BadgeModule, ButtonModule, @@ -168,6 +169,7 @@ export class VaultV2Component private organizations$: Observable = this.accountService.activeAccount$.pipe( map((a) => a?.id), + filterOutNullish(), switchMap((id) => this.organizationService.organizations$(id)), ); @@ -290,7 +292,7 @@ export class VaultV2Component ) { const value = await firstValueFrom( this.totpService.getCode$(this.cipher.login.totp), - ).catch(() => null); + ).catch((): any => null); if (value) { this.copyValue(this.cipher, value.code, "verificationCodeTotp", "TOTP"); } @@ -319,7 +321,7 @@ export class VaultV2Component this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); const authRequests = await firstValueFrom( - this.authRequestService.getLatestPendingAuthRequest$(), + this.authRequestService.getLatestPendingAuthRequest$()!, ); if (authRequests != null) { this.messagingService.send("openLoginApproval", { @@ -329,7 +331,7 @@ export class VaultV2Component this.activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(getUserId), - ).catch(() => null); + ).catch((): any => null); if (this.activeUserId) { this.cipherService @@ -448,7 +450,7 @@ export class VaultV2Component const dialogRef = AttachmentsV2Component.open(this.dialogService, { cipherId: this.cipherId as CipherId, }); - const result = await firstValueFrom(dialogRef.closed).catch(() => null); + const result = await firstValueFrom(dialogRef.closed).catch((): any => null); if ( result?.action === AttachmentDialogResult.Removed || result?.action === AttachmentDialogResult.Uploaded @@ -574,7 +576,7 @@ export class VaultV2Component click: async () => { const value = await firstValueFrom( this.totpService.getCode$(cipher.login.totp), - ).catch(() => null); + ).catch((): any => null); if (value) { this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP"); } @@ -617,7 +619,7 @@ export class VaultV2Component async buildFormConfig(action: CipherFormMode) { this.config = await this.formConfigService .buildConfig(action, this.cipherId as CipherId, this.addType) - .catch(() => null); + .catch((): any => null); } async editCipher(cipher: CipherView) { diff --git a/apps/desktop/webpack.base.js b/apps/desktop/webpack.base.js new file mode 100644 index 00000000000..fe3079b730f --- /dev/null +++ b/apps/desktop/webpack.base.js @@ -0,0 +1,320 @@ +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const { AngularWebpackPlugin } = require("@ngtools/webpack"); +const TerserPlugin = require("terser-webpack-plugin"); +const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); +const { EnvironmentPlugin, DefinePlugin } = require("webpack"); +const configurator = require("./config/config"); + +module.exports.getEnv = function getEnv() { + const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV; + const ENV = process.env.ENV == null ? "development" : process.env.ENV; + + return { NODE_ENV, ENV }; +}; + +/** + * @param {{ + * configName: string; + * renderer: { + * entry: string; + * entryModule: string; + * tsConfig: string; + * }; + * main: { + * entry: string; + * tsConfig: string; + * }; + * preload: { + * entry: string; + * tsConfig: string; + * }; + * }} params + */ +module.exports.buildConfig = function buildConfig(params) { + const { NODE_ENV, ENV } = module.exports.getEnv(); + + console.log(`Building ${params.configName} Desktop App`); + + const envConfig = configurator.load(NODE_ENV); + configurator.log(envConfig); + + const commonConfig = { + resolve: { + extensions: [".tsx", ".ts", ".js"], + symlinks: false, + modules: [path.resolve("../../node_modules")], + }, + }; + + const getOutputConfig = (isDev) => ({ + filename: "[name].js", + path: path.resolve(__dirname, "build"), + ...(isDev && { devtoolModuleFilenameTemplate: "[absolute-resource-path]" }), + }); + + const mainConfig = { + name: "main", + mode: NODE_ENV, + target: "electron-main", + node: { + __dirname: false, + __filename: false, + }, + entry: { + main: params.main.entry, + }, + optimization: { + minimize: false, + }, + output: getOutputConfig(NODE_ENV === "development"), + devtool: NODE_ENV === "development" ? "cheap-source-map" : false, + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules\/(?!(@bitwarden)\/).*/, + }, + { + test: /\.node$/, + loader: "node-loader", + }, + ], + }, + experiments: { + asyncWebAssembly: true, + }, + resolve: { + ...commonConfig.resolve, + plugins: [new TsconfigPathsPlugin({ configFile: params.main.tsConfig })], + }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + "./src/package.json", + { from: "./src/images", to: "images" }, + { from: "./src/locales", to: "locales" }, + ], + }), + new DefinePlugin({ + BIT_ENVIRONMENT: JSON.stringify(NODE_ENV), + }), + new EnvironmentPlugin({ + FLAGS: envConfig.flags, + DEV_FLAGS: NODE_ENV === "development" ? envConfig.devFlags : {}, + }), + ], + externals: { + "electron-reload": "commonjs2 electron-reload", + "@bitwarden/desktop-napi": "commonjs2 @bitwarden/desktop-napi", + }, + }; + + const preloadConfig = { + name: "preload", + mode: NODE_ENV, + target: "electron-preload", + node: { + __dirname: false, + __filename: false, + }, + entry: { + preload: params.preload.entry, + }, + optimization: { + minimize: false, + }, + output: getOutputConfig(NODE_ENV === "development"), + devtool: NODE_ENV === "development" ? "cheap-source-map" : false, + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules\/(?!(@bitwarden)\/).*/, + }, + ], + }, + resolve: { + ...commonConfig.resolve, + plugins: [new TsconfigPathsPlugin({ configFile: params.preload.tsConfig })], + }, + plugins: [ + new DefinePlugin({ + BIT_ENVIRONMENT: JSON.stringify(NODE_ENV), + }), + ], + }; + + const rendererConfig = { + name: "renderer", + mode: NODE_ENV, + devtool: "source-map", + target: "web", + node: { + __dirname: false, + }, + entry: { + "app/main": params.renderer.entry, + }, + output: { + filename: "[name].js", + path: path.resolve(__dirname, "build"), + }, + optimization: { + minimizer: [ + new TerserPlugin({ + terserOptions: { + // Replicate Angular CLI behaviour + compress: { + global_defs: { + ngDevMode: false, + ngI18nClosureMode: false, + }, + }, + }, + }), + ], + splitChunks: { + cacheGroups: { + commons: { + test: /[\\/]node_modules[\\/]/, + name: "app/vendor", + chunks: (chunk) => { + return chunk.name === "app/main"; + }, + }, + }, + }, + }, + module: { + rules: [ + { + test: /\.[cm]?js$/, + use: [ + { + loader: "babel-loader", + options: { + configFile: "../../babel.config.json", + }, + }, + ], + }, + { + test: /\.[jt]sx?$/, + loader: "@ngtools/webpack", + }, + { + test: /\.(html)$/, + loader: "html-loader", + }, + { + test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, + exclude: /loading.svg/, + generator: { + filename: "fonts/[name].[contenthash][ext]", + }, + type: "asset/resource", + }, + { + test: /\.(jpe?g|png|gif|svg)$/i, + exclude: /.*(bwi-font)\.svg/, + generator: { + filename: "images/[name][ext]", + }, + type: "asset/resource", + }, + { + test: /\.css$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + }, + "css-loader", + "resolve-url-loader", + { + loader: "postcss-loader", + options: { + sourceMap: true, + }, + }, + ], + }, + { + test: /\.scss$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + options: { + publicPath: "../", + }, + }, + "css-loader", + "resolve-url-loader", + { + loader: "sass-loader", + options: { + sourceMap: true, + }, + }, + ], + }, + // Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560 + { + test: /[\/\\]@angular[\/\\].+\.js$/, + parser: { system: true }, + }, + ], + }, + experiments: { + asyncWebAssembly: true, + }, + resolve: { + ...commonConfig.resolve, + fallback: { + path: require.resolve("path-browserify"), + fs: false, + }, + }, + plugins: [ + new AngularWebpackPlugin({ + tsConfigPath: params.renderer.tsConfig, + entryModule: params.renderer.entryModule, + sourceMap: true, + }), + // ref: https://github.com/angular/angular/issues/20357 + new webpack.ContextReplacementPlugin( + /\@angular(\\|\/)core(\\|\/)fesm5/, + path.resolve(__dirname, "./src"), + ), + new HtmlWebpackPlugin({ + template: "./src/index.html", + filename: "index.html", + chunks: ["app/vendor", "app/main"], + }), + new webpack.SourceMapDevToolPlugin({ + include: ["app/main.js"], + }), + new MiniCssExtractPlugin({ + filename: "[name].[contenthash].css", + chunkFilename: "[id].[contenthash].css", + }), + new webpack.DefinePlugin({ + BIT_ENVIRONMENT: JSON.stringify(NODE_ENV), + }), + new webpack.EnvironmentPlugin({ + ENV: ENV, + FLAGS: envConfig.flags, + DEV_FLAGS: NODE_ENV === "development" ? envConfig.devFlags : {}, + ADDITIONAL_REGIONS: envConfig.additionalRegions ?? [], + }), + ], + }; + + return [mainConfig, rendererConfig, preloadConfig]; +}; diff --git a/apps/desktop/webpack.config.js b/apps/desktop/webpack.config.js new file mode 100644 index 00000000000..5ba0df337ee --- /dev/null +++ b/apps/desktop/webpack.config.js @@ -0,0 +1,18 @@ +const { buildConfig } = require("./webpack.base"); + +module.exports = buildConfig({ + configName: "OSS", + renderer: { + entry: "./src/app/main.ts", + entryModule: "src/app/app.module#AppModule", + tsConfig: "./tsconfig.renderer.json", + }, + main: { + entry: "./src/entry.ts", + tsConfig: "./tsconfig.json", + }, + preload: { + entry: "./src/preload.ts", + tsConfig: "./tsconfig.json", + }, +}); diff --git a/apps/desktop/webpack.main.js b/apps/desktop/webpack.main.js deleted file mode 100644 index 151b1d0cea2..00000000000 --- a/apps/desktop/webpack.main.js +++ /dev/null @@ -1,93 +0,0 @@ -const path = require("path"); -const { merge } = require("webpack-merge"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); -const configurator = require("./config/config"); -const { EnvironmentPlugin, DefinePlugin } = require("webpack"); - -const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV; - -console.log("Main process config"); -const envConfig = configurator.load(NODE_ENV); -configurator.log(envConfig); - -const common = { - module: { - rules: [ - { - test: /\.tsx?$/, - use: "ts-loader", - exclude: /node_modules\/(?!(@bitwarden)\/).*/, - }, - ], - }, - plugins: [], - resolve: { - extensions: [".tsx", ".ts", ".js"], - plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })], - }, -}; - -const prod = { - output: { - filename: "[name].js", - path: path.resolve(__dirname, "build"), - }, -}; - -const dev = { - output: { - filename: "[name].js", - path: path.resolve(__dirname, "build"), - devtoolModuleFilenameTemplate: "[absolute-resource-path]", - }, - devtool: "cheap-source-map", -}; - -const main = { - mode: NODE_ENV, - target: "electron-main", - node: { - __dirname: false, - __filename: false, - }, - entry: { - main: "./src/entry.ts", - }, - optimization: { - minimize: false, - }, - module: { - rules: [ - { - test: /\.node$/, - loader: "node-loader", - }, - ], - }, - experiments: { - asyncWebAssembly: true, - }, - plugins: [ - new CopyWebpackPlugin({ - patterns: [ - "./src/package.json", - { from: "./src/images", to: "images" }, - { from: "./src/locales", to: "locales" }, - ], - }), - new DefinePlugin({ - BIT_ENVIRONMENT: JSON.stringify(NODE_ENV), - }), - new EnvironmentPlugin({ - FLAGS: envConfig.flags, - DEV_FLAGS: NODE_ENV === "development" ? envConfig.devFlags : {}, - }), - ], - externals: { - "electron-reload": "commonjs2 electron-reload", - "@bitwarden/desktop-napi": "commonjs2 @bitwarden/desktop-napi", - }, -}; - -module.exports = merge(common, NODE_ENV === "development" ? dev : prod, main); diff --git a/apps/desktop/webpack.preload.js b/apps/desktop/webpack.preload.js deleted file mode 100644 index db75e882644..00000000000 --- a/apps/desktop/webpack.preload.js +++ /dev/null @@ -1,66 +0,0 @@ -const path = require("path"); -const { merge } = require("webpack-merge"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); -const configurator = require("./config/config"); -const { EnvironmentPlugin, DefinePlugin } = require("webpack"); - -const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV; - -console.log("Preload process config"); -const envConfig = configurator.load(NODE_ENV); -configurator.log(envConfig); - -const common = { - module: { - rules: [ - { - test: /\.tsx?$/, - use: "ts-loader", - exclude: /node_modules\/(?!(@bitwarden)\/).*/, - }, - ], - }, - plugins: [ - new DefinePlugin({ - BIT_ENVIRONMENT: JSON.stringify(NODE_ENV), - }), - ], - resolve: { - extensions: [".tsx", ".ts", ".js"], - plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })], - }, -}; - -const prod = { - output: { - filename: "[name].js", - path: path.resolve(__dirname, "build"), - }, -}; - -const dev = { - output: { - filename: "[name].js", - path: path.resolve(__dirname, "build"), - devtoolModuleFilenameTemplate: "[absolute-resource-path]", - }, - devtool: "cheap-source-map", -}; - -const main = { - mode: NODE_ENV, - target: "electron-preload", - node: { - __dirname: false, - __filename: false, - }, - entry: { - preload: "./src/preload.ts", - }, - optimization: { - minimize: false, - }, -}; - -module.exports = merge(common, NODE_ENV === "development" ? dev : prod, main); diff --git a/apps/desktop/webpack.renderer.js b/apps/desktop/webpack.renderer.js deleted file mode 100644 index 9c5b0fd2584..00000000000 --- a/apps/desktop/webpack.renderer.js +++ /dev/null @@ -1,192 +0,0 @@ -const path = require("path"); -const webpack = require("webpack"); -const { merge } = require("webpack-merge"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); -const { AngularWebpackPlugin } = require("@ngtools/webpack"); -const TerserPlugin = require("terser-webpack-plugin"); -const configurator = require("./config/config"); - -const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV; - -console.log("Renderer process config"); -const envConfig = configurator.load(NODE_ENV); -configurator.log(envConfig); - -const ENV = process.env.ENV == null ? "development" : process.env.ENV; - -const common = { - module: { - rules: [ - { - test: /\.[cm]?js$/, - use: [ - { - loader: "babel-loader", - options: { - configFile: "../../babel.config.json", - }, - }, - ], - }, - { - test: /\.[jt]sx?$/, - loader: "@ngtools/webpack", - }, - { - test: /\.(jpe?g|png|gif|svg)$/i, - exclude: /.*(bwi-font)\.svg/, - generator: { - filename: "images/[name][ext]", - }, - type: "asset/resource", - }, - ], - }, - plugins: [], - resolve: { - extensions: [".tsx", ".ts", ".js"], - symlinks: false, - modules: [path.resolve("../../node_modules")], - fallback: { - path: require.resolve("path-browserify"), - fs: false, - }, - }, - output: { - filename: "[name].js", - path: path.resolve(__dirname, "build"), - }, -}; - -const renderer = { - mode: NODE_ENV, - devtool: "source-map", - target: "web", - node: { - __dirname: false, - }, - entry: { - "app/main": "./src/app/main.ts", - }, - optimization: { - minimizer: [ - new TerserPlugin({ - terserOptions: { - // Replicate Angular CLI behaviour - compress: { - global_defs: { - ngDevMode: false, - ngI18nClosureMode: false, - }, - }, - }, - }), - ], - splitChunks: { - cacheGroups: { - commons: { - test: /[\\/]node_modules[\\/]/, - name: "app/vendor", - chunks: (chunk) => { - return chunk.name === "app/main"; - }, - }, - }, - }, - }, - module: { - rules: [ - { - test: /\.(html)$/, - loader: "html-loader", - }, - { - test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, - exclude: /loading.svg/, - generator: { - filename: "fonts/[name].[contenthash][ext]", - }, - type: "asset/resource", - }, - { - test: /\.css$/, - use: [ - { - loader: MiniCssExtractPlugin.loader, - }, - "css-loader", - "resolve-url-loader", - { - loader: "postcss-loader", - options: { - sourceMap: true, - }, - }, - ], - }, - { - test: /\.scss$/, - use: [ - { - loader: MiniCssExtractPlugin.loader, - options: { - publicPath: "../", - }, - }, - "css-loader", - "resolve-url-loader", - { - loader: "sass-loader", - options: { - sourceMap: true, - }, - }, - ], - }, - // Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560 - { - test: /[\/\\]@angular[\/\\].+\.js$/, - parser: { system: true }, - }, - ], - }, - experiments: { - asyncWebAssembly: true, - }, - plugins: [ - new AngularWebpackPlugin({ - tsConfigPath: "tsconfig.renderer.json", - entryModule: "src/app/app.module#AppModule", - sourceMap: true, - }), - // ref: https://github.com/angular/angular/issues/20357 - new webpack.ContextReplacementPlugin( - /\@angular(\\|\/)core(\\|\/)fesm5/, - path.resolve(__dirname, "./src"), - ), - new HtmlWebpackPlugin({ - template: "./src/index.html", - filename: "index.html", - chunks: ["app/vendor", "app/main"], - }), - new webpack.SourceMapDevToolPlugin({ - include: ["app/main.js"], - }), - new MiniCssExtractPlugin({ - filename: "[name].[contenthash].css", - chunkFilename: "[id].[contenthash].css", - }), - new webpack.DefinePlugin({ - BIT_ENVIRONMENT: JSON.stringify(NODE_ENV), - }), - new webpack.EnvironmentPlugin({ - ENV: ENV, - FLAGS: envConfig.flags, - DEV_FLAGS: NODE_ENV === "development" ? envConfig.devFlags : {}, - ADDITIONAL_REGIONS: envConfig.additionalRegions ?? [], - }), - ], -}; - -module.exports = merge(common, renderer); diff --git a/apps/web/CLAUDE.md b/apps/web/CLAUDE.md new file mode 100644 index 00000000000..b9fe0055fe5 --- /dev/null +++ b/apps/web/CLAUDE.md @@ -0,0 +1,13 @@ +# Web Vault - Critical Rules + +- **NEVER** access browser extension APIs + - Web vault runs in standard browser context (no chrome._/browser._ APIs) + - DON'T import or use BrowserApi or extension-specific code + +- **ALWAYS** assume multi-tenant organization features + - Web vault supports enterprise organizations with complex permissions + - Use organization permission guards: `/apps/web/src/app/admin-console/organizations/guards/` + +- **CRITICAL**: All sensitive operations must work without local storage + - Web vault may run in environments that clear storage aggressively + - DON'T rely on localStorage/sessionStorage for security-critical data diff --git a/apps/web/package.json b/apps/web/package.json index 517b8aa8004..5690ce77dec 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2025.9.1", + "version": "2025.10.0", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html b/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html deleted file mode 100644 index 326dc627e17..00000000000 --- a/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html +++ /dev/null @@ -1,121 +0,0 @@ -@if (organization) { - - - - -} - - - -
-
- -
-
- - - {{ "all" | i18n }} - - - - {{ "addAccess" | i18n }} - - - - {{ trashCleanupWarning }} - - - - - - {{ "noItemsInList" | i18n }} - - - - - -
- - {{ "loading" | i18n }} -
-
-
diff --git a/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.ts deleted file mode 100644 index fce2827c073..00000000000 --- a/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.ts +++ /dev/null @@ -1,1389 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import { ActivatedRoute, Params, Router } from "@angular/router"; -import { - BehaviorSubject, - combineLatest, - firstValueFrom, - lastValueFrom, - merge, - Observable, - Subject, -} from "rxjs"; -import { - concatMap, - debounceTime, - distinctUntilChanged, - filter, - first, - map, - shareReplay, - switchMap, - takeUntil, - tap, -} from "rxjs/operators"; - -import { - CollectionAdminService, - CollectionAdminView, - CollectionService, - CollectionView, - Unassigned, -} from "@bitwarden/admin-console/common"; -import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; -import { NoResults } from "@bitwarden/assets/svg"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.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 { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; -import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SyncService } from "@bitwarden/common/platform/sync"; -import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; -import { - BannerModule, - DialogRef, - DialogService, - NoItemsModule, - ToastService, -} from "@bitwarden/components"; -import { - AttachmentDialogResult, - AttachmentsV2Component, - CipherFormConfig, - CipherFormConfigService, - CollectionAssignmentResult, - DecryptionFailureDialogComponent, - PasswordRepromptService, -} from "@bitwarden/vault"; -import { - OrganizationFreeTrialWarningComponent, - OrganizationResellerRenewalWarningComponent, -} from "@bitwarden/web-vault/app/billing/organizations/warnings/components"; -import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; -import { VaultItemsComponent } from "@bitwarden/web-vault/app/vault/components/vault-items/vault-items.component"; - -import { SharedModule } from "../../../shared"; -import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections"; -import { - VaultItemDialogComponent, - VaultItemDialogMode, - VaultItemDialogResult, -} from "../../../vault/components/vault-item-dialog/vault-item-dialog.component"; -import { VaultItemEvent } from "../../../vault/components/vault-items/vault-item-event"; -import { VaultItemsModule } from "../../../vault/components/vault-items/vault-items.module"; -import { - BulkDeleteDialogResult, - openBulkDeleteDialog, -} from "../../../vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component"; -import { VaultFilterService } from "../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; -import { RoutedVaultFilterBridgeService } from "../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; -import { RoutedVaultFilterService } from "../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; -import { createFilterFunction } from "../../../vault/individual-vault/vault-filter/shared/models/filter-function"; -import { - All, - RoutedVaultFilterModel, -} from "../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; -import { VaultFilter } from "../../../vault/individual-vault/vault-filter/shared/models/vault-filter.model"; -import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service"; -import { GroupApiService, GroupView } from "../core"; -import { openEntityEventsDialog } from "../manage/entity-events.component"; -import { - CollectionDialogAction, - CollectionDialogTabType, - openCollectionDialog, -} from "../shared/components/collection-dialog"; - -import { - BulkCollectionsDialogComponent, - BulkCollectionsDialogResult, -} from "./bulk-collections-dialog"; -import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component"; -import { getFlatCollectionTree, getNestedCollectionTree } from "./utils"; -import { VaultFilterModule } from "./vault-filter/vault-filter.module"; -import { VaultHeaderComponent } from "./vault-header/vault-header.component"; - -const BroadcasterSubscriptionId = "OrgVaultComponent"; -const SearchTextDebounceInterval = 200; - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -enum AddAccessStatusType { - All = 0, - AddAccess = 1, -} - -@Component({ - selector: "app-org-vault", - templateUrl: "deprecated_vault.component.html", - imports: [ - VaultHeaderComponent, - CollectionAccessRestrictedComponent, - VaultFilterModule, - VaultItemsModule, - SharedModule, - BannerModule, - NoItemsModule, - OrganizationFreeTrialWarningComponent, - OrganizationResellerRenewalWarningComponent, - ], - providers: [ - RoutedVaultFilterService, - RoutedVaultFilterBridgeService, - { provide: CipherFormConfigService, useClass: AdminConsoleCipherFormConfigService }, - ], -}) -export class VaultComponent implements OnInit, OnDestroy { - protected Unassigned = Unassigned; - - trashCleanupWarning: string = null; - activeFilter: VaultFilter = new VaultFilter(); - - protected showAddAccessToggle = false; - protected noItemIcon = NoResults; - protected performingInitialLoad = true; - protected refreshing = false; - protected processingEvent = false; - protected filter: RoutedVaultFilterModel = {}; - protected organization: Organization; - protected allCollections: CollectionAdminView[]; - protected allGroups: GroupView[]; - protected ciphers: CipherView[]; - protected collections: CollectionAdminView[]; - protected selectedCollection: TreeNode | undefined; - protected isEmpty: boolean; - protected showCollectionAccessRestricted: boolean; - protected currentSearchText$: Observable; - protected prevCipherId: string | null = null; - protected userId: UserId; - /** - * A list of collections that the user can assign items to and edit those items within. - * @protected - */ - protected editableCollections$: Observable; - protected allCollectionsWithoutUnassigned$: Observable; - - protected get hideVaultFilters(): boolean { - return this.organization?.isProviderUser && !this.organization?.isMember; - } - - private searchText$ = new Subject(); - private refresh$ = new BehaviorSubject(null); - private destroy$ = new Subject(); - protected addAccessStatus$ = new BehaviorSubject(0); - private vaultItemDialogRef?: DialogRef | undefined; - - @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; - - constructor( - private route: ActivatedRoute, - private organizationService: OrganizationService, - protected vaultFilterService: VaultFilterService, - private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService, - private routedVaultFilterService: RoutedVaultFilterService, - private router: Router, - private changeDetectorRef: ChangeDetectorRef, - private syncService: SyncService, - private i18nService: I18nService, - private dialogService: DialogService, - private messagingService: MessagingService, - private broadcasterService: BroadcasterService, - private ngZone: NgZone, - private platformUtilsService: PlatformUtilsService, - private cipherService: CipherService, - private passwordRepromptService: PasswordRepromptService, - private collectionAdminService: CollectionAdminService, - private searchService: SearchService, - private searchPipe: SearchPipe, - private groupService: GroupApiService, - private logService: LogService, - private eventCollectionService: EventCollectionService, - private totpService: TotpService, - private apiService: ApiService, - private toastService: ToastService, - private configService: ConfigService, - private cipherFormConfigService: CipherFormConfigService, - protected billingApiService: BillingApiServiceAbstraction, - private accountService: AccountService, - private organizationWarningsService: OrganizationWarningsService, - private collectionService: CollectionService, - ) {} - - async ngOnInit() { - this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - - this.trashCleanupWarning = this.i18nService.t( - this.platformUtilsService.isSelfHost() - ? "trashCleanupWarningSelfHosted" - : "trashCleanupWarning", - ); - - const filter$ = this.routedVaultFilterService.filter$; - - // FIXME: The RoutedVaultFilterModel uses `organizationId: Unassigned` to represent the individual vault, - // but that is never used in Admin Console. This function narrows the type so it doesn't pollute our code here, - // but really we should change to using our own vault filter model that only represents valid states in AC. - const isOrganizationId = (value: OrganizationId | Unassigned): value is OrganizationId => - value !== Unassigned; - const organizationId$ = filter$.pipe( - map((filter) => filter.organizationId), - filter((filter) => filter !== undefined), - filter(isOrganizationId), - distinctUntilChanged(), - ); - - 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( - first(), - switchMap(async ([organization]) => { - this.organization = organization; - - if (!organization.canEditAnyCollection) { - await this.syncService.fullSync(false); - } - - return undefined; - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - // 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.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - if (message.successfully) { - this.refresh(); - this.changeDetectorRef.detectChanges(); - } - break; - } - }); - }); - - this.routedVaultFilterBridgeService.activeFilter$ - .pipe(takeUntil(this.destroy$)) - .subscribe((activeFilter) => { - this.activeFilter = activeFilter; - - // watch the active filters. Only show toggle when viewing the collections filter - if (!this.activeFilter.collectionId) { - this.showAddAccessToggle = false; - } - }); - - this.searchText$ - .pipe(debounceTime(SearchTextDebounceInterval), takeUntil(this.destroy$)) - .subscribe((searchText) => - this.router.navigate([], { - queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText }, - queryParamsHandling: "merge", - replaceUrl: true, - }), - ); - - this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search)); - - this.allCollectionsWithoutUnassigned$ = this.refresh$.pipe( - switchMap(() => organizationId$), - switchMap((orgId) => - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.collectionAdminService.collectionAdminViews$(orgId, userId)), - ), - ), - shareReplay({ refCount: false, bufferSize: 1 }), - ); - - this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe( - map((collections) => { - // Users that can edit all ciphers can implicitly add to / edit within any collection - if (this.organization.canEditAllCiphers) { - return collections; - } - return collections.filter((c) => c.assigned); - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const allCollections$ = combineLatest([ - organizationId$, - this.allCollectionsWithoutUnassigned$, - ]).pipe( - map(([organizationId, allCollections]) => { - // FIXME: We should not assert that the Unassigned type is a CollectionId. - // Instead we should consider representing the Unassigned collection as a different object, given that - // it is not actually a collection. - return allCollections.concat( - new CollectionAdminView({ - name: this.i18nService.t("unassigned"), - id: Unassigned as CollectionId, - organizationId, - }), - ); - }), - ); - - const allGroups$ = organizationId$.pipe( - switchMap((organizationId) => this.groupService.getAll(organizationId)), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const allCiphers$ = combineLatest([organization$, this.refresh$]).pipe( - switchMap(async ([organization]) => { - // If user swaps organization reset the addAccessToggle - if (!this.showAddAccessToggle || organization) { - this.addAccessToggle(0); - } - let ciphers; - - // Restricted providers (who are not members) do not have access org cipher endpoint below - // Return early to avoid 404 response - if (!organization.isMember && organization.isProviderUser) { - return []; - } - - // If the user can edit all ciphers for the organization then fetch them ALL. - if (organization.canEditAllCiphers) { - ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id); - ciphers?.forEach((c) => (c.edit = true)); - } else { - // Otherwise, only fetch ciphers they have access to (includes unassigned for admins). - ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id); - } - - await this.searchService.indexCiphers(this.userId, ciphers, organization.id); - return ciphers; - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const allCipherMap$ = allCiphers$.pipe( - map((ciphers) => { - return Object.fromEntries(ciphers.map((c) => [c.id, c])); - }), - ); - - const nestedCollections$ = allCollections$.pipe( - map((collections) => getNestedCollectionTree(collections)), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const collections$ = combineLatest([ - nestedCollections$, - filter$, - this.currentSearchText$, - this.addAccessStatus$, - ]).pipe( - filter(([collections, filter]) => collections != undefined && filter != undefined), - concatMap(async ([collections, filter, searchText, addAccessStatus]) => { - if ( - filter.collectionId === Unassigned || - (filter.collectionId === undefined && filter.type !== undefined) - ) { - return []; - } - - this.showAddAccessToggle = false; - let searchableCollectionNodes: TreeNode[] = []; - if (filter.collectionId === undefined || filter.collectionId === All) { - searchableCollectionNodes = collections; - } else { - const selectedCollection = ServiceUtils.getTreeNodeObjectFromList( - collections, - filter.collectionId, - ); - searchableCollectionNodes = selectedCollection?.children ?? []; - } - - let collectionsToReturn: CollectionAdminView[] = []; - - if (await this.searchService.isSearchable(this.userId, searchText)) { - // Flatten the tree for searching through all levels - const flatCollectionTree: CollectionAdminView[] = - getFlatCollectionTree(searchableCollectionNodes); - - collectionsToReturn = this.searchPipe.transform( - flatCollectionTree, - searchText, - (collection) => collection.name, - (collection) => collection.id, - ); - } else { - collectionsToReturn = searchableCollectionNodes.map( - (treeNode: TreeNode): CollectionAdminView => treeNode.node, - ); - } - - // Add access toggle is only shown if allowAdminAccessToAllCollectionItems is false and there are unmanaged collections the user can edit - this.showAddAccessToggle = - !this.organization.allowAdminAccessToAllCollectionItems && - this.organization.canEditUnmanagedCollections && - collectionsToReturn.some((c) => c.unmanaged); - - if (addAccessStatus === 1 && this.showAddAccessToggle) { - collectionsToReturn = collectionsToReturn.filter((c) => c.unmanaged); - } - return collectionsToReturn; - }), - takeUntil(this.destroy$), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const selectedCollection$ = combineLatest([nestedCollections$, filter$]).pipe( - filter(([collections, filter]) => collections != undefined && filter != undefined), - map(([collections, filter]) => { - if ( - filter.collectionId === undefined || - filter.collectionId === All || - filter.collectionId === Unassigned - ) { - return undefined; - } - - return ServiceUtils.getTreeNodeObjectFromList(collections, filter.collectionId); - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const showCollectionAccessRestricted$ = combineLatest([ - filter$, - selectedCollection$, - organization$, - ]).pipe( - map(([filter, collection, organization]) => { - return ( - (filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers) || - (!organization.canEditAllCiphers && collection != undefined && !collection.node.assigned) - ); - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - const ciphers$ = combineLatest([ - allCiphers$, - filter$, - this.currentSearchText$, - showCollectionAccessRestricted$, - ]).pipe( - filter(([ciphers, filter]) => ciphers != undefined && filter != undefined), - concatMap(async ([ciphers, filter, searchText, showCollectionAccessRestricted]) => { - if (filter.collectionId === undefined && filter.type === undefined) { - return []; - } - - if (showCollectionAccessRestricted) { - // Do not show ciphers for restricted collections - // Ciphers belonging to multiple collections may still be present in $allCiphers and shouldn't be visible - return []; - } - - const filterFunction = createFilterFunction(filter); - - if (await this.searchService.isSearchable(this.userId, searchText)) { - return await this.searchService.searchCiphers( - this.userId, - searchText, - [filterFunction], - ciphers, - ); - } - - return ciphers.filter(filterFunction); - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - - firstSetup$ - .pipe( - switchMap(() => combineLatest([this.route.queryParams, allCipherMap$])), - filter(() => this.vaultItemDialogRef == undefined), - switchMap(async ([qParams, allCiphersMap]) => { - const cipherId = getCipherIdFromParams(qParams); - - if (!cipherId) { - this.prevCipherId = null; - return; - } - - if (cipherId === this.prevCipherId) { - return; - } - - this.prevCipherId = cipherId; - - const cipher = allCiphersMap[cipherId]; - if (cipher) { - let action = qParams.action; - - if (action == "showFailedToDecrypt") { - DecryptionFailureDialogComponent.open(this.dialogService, { - cipherIds: [cipherId as CipherId], - }); - await this.router.navigate([], { - queryParams: { itemId: null, cipherId: null, action: null }, - queryParamsHandling: "merge", - replaceUrl: true, - }); - return; - } - - // Default to "view" - if (action == null) { - action = "view"; - } - - if (action === "view") { - await this.viewCipherById(cipher); - } else { - await this.editCipher(cipher, false); - } - } else { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unknownCipher"), - }); - await this.router.navigate([], { - queryParams: { cipherId: null, itemId: null }, - queryParamsHandling: "merge", - }); - } - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - firstSetup$ - .pipe( - switchMap(() => combineLatest([this.route.queryParams, organization$, allCiphers$])), - switchMap(async ([qParams, organization, allCiphers$]) => { - const cipherId = qParams.viewEvents; - if (!cipherId) { - return; - } - const cipher = allCiphers$.find((c) => c.id === cipherId); - if (organization.useEvents && cipher != undefined) { - await this.viewEvents(cipher); - } else { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unknownCipher"), - }); - await this.router.navigate([], { - queryParams: { viewEvents: null }, - queryParamsHandling: "merge", - }); - } - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - // Billing Warnings - organization$ - .pipe( - switchMap((organization) => - merge( - this.organizationWarningsService.showInactiveSubscriptionDialog$(organization), - this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), - ), - ), - takeUntil(this.destroy$), - ) - .subscribe(); - // End Billing Warnings - - firstSetup$ - .pipe( - switchMap(() => this.refresh$), - tap(() => (this.refreshing = true)), - switchMap(() => - combineLatest([ - organization$, - filter$, - allCollections$, - allGroups$, - ciphers$, - collections$, - selectedCollection$, - showCollectionAccessRestricted$, - ]), - ), - takeUntil(this.destroy$), - ) - .subscribe( - ([ - organization, - filter, - allCollections, - allGroups, - ciphers, - collections, - selectedCollection, - showCollectionAccessRestricted, - ]) => { - this.organization = organization; - this.filter = filter; - this.allCollections = allCollections; - this.allGroups = allGroups; - this.ciphers = ciphers; - this.collections = collections; - this.selectedCollection = selectedCollection; - this.showCollectionAccessRestricted = showCollectionAccessRestricted; - - this.isEmpty = collections?.length === 0 && ciphers?.length === 0; - - // This is a temporary fix to avoid double fetching collections. - // TODO: Remove when implementing new VVR menu - this.vaultFilterService.reloadCollections(allCollections); - - this.refreshing = false; - this.performingInitialLoad = false; - }, - ); - } - - async navigateToPaymentMethod() { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; - await this.router.navigate(["organizations", `${this.organization?.id}`, "billing", route], { - state: { launchPaymentModalAutomatically: true }, - }); - } - - addAccessToggle(e: AddAccessStatusType) { - this.addAccessStatus$.next(e); - } - - get loading() { - return this.refreshing || this.processingEvent; - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - this.destroy$.next(); - this.destroy$.complete(); - } - - async onVaultItemsEvent(event: VaultItemEvent) { - this.processingEvent = true; - - try { - switch (event.type) { - case "viewAttachments": - await this.editCipherAttachments(event.item); - break; - case "clone": - await this.cloneCipher(event.item); - break; - case "restore": - if (event.items.length === 1) { - await this.restore(event.items[0]); - } else { - await this.bulkRestore(event.items); - } - break; - case "delete": { - const ciphers = event.items - .filter((i) => i.collection === undefined) - .map((i) => i.cipher); - const collections = event.items - .filter((i) => i.cipher === undefined) - .map((i) => i.collection); - if (ciphers.length === 1 && collections.length === 0) { - await this.deleteCipher(ciphers[0]); - } else if (ciphers.length === 0 && collections.length === 1) { - await this.deleteCollection(collections[0] as CollectionAdminView); - } else { - await this.bulkDelete(ciphers, collections, this.organization); - } - break; - } - case "copyField": - await this.copy(event.item, event.field); - break; - case "editCollection": - await this.editCollection( - event.item as CollectionAdminView, - CollectionDialogTabType.Info, - event.readonly, - ); - break; - case "viewCollectionAccess": - await this.editCollection( - event.item as CollectionAdminView, - CollectionDialogTabType.Access, - event.readonly, - ); - break; - case "bulkEditCollectionAccess": - await this.bulkEditCollectionAccess(event.items, this.organization); - break; - case "assignToCollections": - await this.bulkAssignToCollections(event.items); - break; - case "viewEvents": - await this.viewEvents(event.item); - break; - } - } finally { - this.processingEvent = false; - } - } - - filterSearchText(searchText: string) { - this.searchText$.next(searchText); - } - - async editCipherAttachments(cipher: CipherView) { - if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - this.go({ cipherId: null, itemId: null }); - return; - } - - if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) { - this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId }); - return; - } - - const dialogRef = AttachmentsV2Component.open(this.dialogService, { - cipherId: cipher.id as CipherId, - organizationId: cipher.organizationId as OrganizationId, - admin: true, - }); - - const result = await firstValueFrom(dialogRef.closed); - - if ( - result.action === AttachmentDialogResult.Removed || - result.action === AttachmentDialogResult.Uploaded - ) { - this.refresh(); - } - } - - /** Opens the Add/Edit Dialog */ - async addCipher(cipherType?: CipherType) { - const cipherFormConfig = await this.cipherFormConfigService.buildConfig( - "add", - null, - cipherType, - ); - - const collectionId: CollectionId | undefined = this.activeFilter.collectionId as CollectionId; - - cipherFormConfig.initialValues = { - organizationId: this.organization.id as OrganizationId, - collectionIds: collectionId ? [collectionId] : [], - }; - - await this.openVaultItemDialog("form", cipherFormConfig); - } - - /** - * Edit the given cipher or add a new cipher - * @param cipherView - When set, the cipher to be edited - * @param cloneCipher - `true` when the cipher should be cloned. - */ - async editCipher(cipher: CipherView | null, cloneCipher: boolean) { - if ( - cipher && - cipher.reprompt !== 0 && - !(await this.passwordRepromptService.showPasswordPrompt()) - ) { - // didn't pass password prompt, so don't open add / edit modal - this.go({ cipherId: null, itemId: null }); - return; - } - - const cipherFormConfig = await this.cipherFormConfigService.buildConfig( - cloneCipher ? "clone" : "edit", - cipher?.id as CipherId | null, - ); - - await this.openVaultItemDialog("form", cipherFormConfig, cipher); - } - - /** Opens the view dialog for the given cipher unless password reprompt fails */ - async viewCipherById(cipher: CipherView) { - if (!cipher) { - return; - } - - if ( - cipher && - cipher.reprompt !== 0 && - !(await this.passwordRepromptService.showPasswordPrompt()) - ) { - // Didn't pass password prompt, so don't open add / edit modal. - await this.go({ cipherId: null, itemId: null, action: null }); - return; - } - - const cipherFormConfig = await this.cipherFormConfigService.buildConfig( - "edit", - cipher.id as CipherId, - cipher.type, - ); - - await this.openVaultItemDialog( - "view", - cipherFormConfig, - cipher, - this.activeFilter.collectionId as CollectionId, - ); - } - - /** - * Open the combined view / edit dialog for a cipher. - */ - async openVaultItemDialog( - mode: VaultItemDialogMode, - formConfig: CipherFormConfig, - cipher?: CipherView, - activeCollectionId?: CollectionId, - ) { - const disableForm = cipher ? !cipher.edit && !this.organization.canEditAllCiphers : false; - // If the form is disabled, force the mode into `view` - const dialogMode = disableForm ? "view" : mode; - this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, { - mode: dialogMode, - formConfig, - disableForm, - activeCollectionId, - isAdminConsoleAction: true, - restore: this.restore, - }); - - const result = await lastValueFrom(this.vaultItemDialogRef.closed); - this.vaultItemDialogRef = undefined; - - // If the dialog was closed by deleting the cipher, refresh the vault. - if (result === VaultItemDialogResult.Deleted || result === VaultItemDialogResult.Saved) { - this.refresh(); - } - - // Clear the query params when the dialog closes - await this.go({ cipherId: null, itemId: null, action: null }); - } - - async cloneCipher(cipher: CipherView) { - if (cipher.login?.hasFido2Credentials) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "passkeyNotCopied" }, - content: { key: "passkeyNotCopiedAlert" }, - type: "info", - }); - - if (!confirmed) { - return false; - } - } - - await this.editCipher(cipher, true); - } - - restore = async (c: CipherView): Promise => { - if (!c.isDeleted) { - return; - } - - if ( - !this.organization.permissions.editAnyCollection && - !c.edit && - !this.organization.allowAdminAccessToAllCollectionItems - ) { - this.showMissingPermissionsError(); - return; - } - - if (!(await this.repromptCipher([c]))) { - return; - } - - // Allow restore of an Unassigned Item - try { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const asAdmin = this.organization?.canEditAnyCollection || c.isUnassigned; - await this.cipherService.restoreWithServer(c.id, activeUserId, asAdmin); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("restoredItem"), - }); - this.refresh(); - } catch (e) { - this.logService.error(e); - } - }; - - async bulkRestore(ciphers: CipherView[]) { - if ( - !this.organization.permissions.editAnyCollection && - ciphers.some((c) => !c.edit && !this.organization.allowAdminAccessToAllCollectionItems) - ) { - this.showMissingPermissionsError(); - return; - } - - if (!(await this.repromptCipher(ciphers))) { - return; - } - - // assess if there are unassigned ciphers and/or editable ciphers selected in bulk for restore - const editAccessCiphers: string[] = []; - const unassignedCiphers: string[] = []; - - // If user has edit all Access no need to check for unassigned ciphers - if (this.organization.canEditAllCiphers) { - ciphers.map((cipher) => { - editAccessCiphers.push(cipher.id); - }); - } else { - ciphers.map((cipher) => { - if (cipher.collectionIds.length === 0) { - unassignedCiphers.push(cipher.id); - } else if (cipher.edit) { - editAccessCiphers.push(cipher.id); - } - }); - } - - if (unassignedCiphers.length === 0 && editAccessCiphers.length === 0) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("nothingSelected"), - }); - return; - } - - if (unassignedCiphers.length > 0 || editAccessCiphers.length > 0) { - await this.cipherService.restoreManyWithServer( - [...unassignedCiphers, ...editAccessCiphers], - this.userId, - this.organization.id, - ); - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("restoredItems"), - }); - this.refresh(); - } - - async deleteCipher(c: CipherView): Promise { - if (!c.edit && !this.organization.canEditAllCiphers) { - this.showMissingPermissionsError(); - return; - } - - if (!(await this.repromptCipher([c]))) { - return; - } - - const permanent = c.isDeleted; - - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: permanent ? "permanentlyDeleteItem" : "deleteItem" }, - content: { key: permanent ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - try { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - await this.deleteCipherWithServer(c.id, activeUserId, permanent, c.isUnassigned); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"), - }); - this.refresh(); - } catch (e) { - this.logService.error(e); - } - } - - async deleteCollection(collection: CollectionAdminView): Promise { - if (!collection.canDelete(this.organization)) { - this.showMissingPermissionsError(); - return; - } - const confirmed = await this.dialogService.openSimpleDialog({ - title: collection.name, - content: { key: "deleteCollectionConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - try { - await this.apiService.deleteCollection(this.organization?.id, collection.id); - await this.collectionService.delete([collection.id as CollectionId], this.userId); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("deletedCollectionId", collection.name), - }); - - // Clear the cipher cache to clear the deleted collection from the cipher state - await this.cipherService.clear(); - - // Navigate away if we deleted the collection we were viewing - if (this.selectedCollection?.node.id === collection.id) { - void this.router.navigate([], { - queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, - queryParamsHandling: "merge", - replaceUrl: true, - }); - } - - this.refresh(); - } catch (e) { - this.logService.error(e); - } - } - - async bulkDelete( - ciphers: CipherView[], - collections: CollectionView[], - organization: Organization, - ) { - if (!(await this.repromptCipher(ciphers))) { - return; - } - - // Allow bulk deleting of Unassigned Items - const unassignedCiphers: string[] = []; - const assignedCiphers: string[] = []; - - ciphers.map((c) => { - if (c.isUnassigned) { - unassignedCiphers.push(c.id); - } else { - assignedCiphers.push(c.id); - } - }); - - if (ciphers.length === 0 && collections.length === 0) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("nothingSelected"), - }); - return; - } - - const canDeleteCollections = - collections == null || collections.every((c) => c.canDelete(organization)); - const canDeleteCiphers = - ciphers == null || ciphers.every((c) => c.edit) || this.organization.canEditAllCiphers; - - if (!canDeleteCiphers || !canDeleteCollections) { - this.showMissingPermissionsError(); - return; - } - - const dialog = openBulkDeleteDialog(this.dialogService, { - data: { - permanent: this.filter.type === "trash", - cipherIds: assignedCiphers, - collections: collections, - organization, - unassignedCiphers, - }, - }); - - const result = await lastValueFrom(dialog.closed); - if (result === BulkDeleteDialogResult.Deleted) { - this.refresh(); - } - } - - async copy(cipher: CipherView, field: "username" | "password" | "totp") { - let aType; - let value; - let typeI18nKey; - - if (field === "username") { - aType = "Username"; - value = cipher.login.username; - typeI18nKey = "username"; - } else if (field === "password") { - aType = "Password"; - value = cipher.login.password; - typeI18nKey = "password"; - } else if (field === "totp") { - aType = "TOTP"; - const totpResponse = await firstValueFrom(this.totpService.getCode$(cipher.login.totp)); - value = totpResponse?.code; - typeI18nKey = "verificationCodeTotp"; - } else { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unexpectedError"), - }); - return; - } - - if ( - this.passwordRepromptService.protectedFields().includes(aType) && - !(await this.repromptCipher([cipher])) - ) { - return; - } - - if (!cipher.viewPassword) { - return; - } - - this.platformUtilsService.copyToClipboard(value, { window: window }); - this.toastService.showToast({ - variant: "info", - title: null, - message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - }); - - if (field === "password") { - await this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id); - } else if (field === "totp") { - await this.eventCollectionService.collect( - EventType.Cipher_ClientCopiedHiddenField, - cipher.id, - ); - } - } - - async addCollection(): Promise { - const dialog = openCollectionDialog(this.dialogService, { - data: { - organizationId: this.organization?.id, - parentCollectionId: this.selectedCollection?.node.id, - limitNestedCollections: !this.organization.canEditAnyCollection, - isAdminConsoleActive: true, - }, - }); - - const result = await lastValueFrom(dialog.closed); - if ( - result.action === CollectionDialogAction.Saved || - result.action === CollectionDialogAction.Deleted - ) { - this.refresh(); - } - } - - async editCollection( - c: CollectionAdminView, - tab: CollectionDialogTabType, - readonly: boolean, - ): Promise { - const dialog = openCollectionDialog(this.dialogService, { - data: { - collectionId: c?.id, - organizationId: this.organization?.id, - initialTab: tab, - readonly: readonly, - isAddAccessCollection: c.unmanaged, - limitNestedCollections: !this.organization.canEditAnyCollection, - isAdminConsoleActive: true, - }, - }); - - const result = await lastValueFrom(dialog.closed); - if ( - result.action === CollectionDialogAction.Saved || - result.action === CollectionDialogAction.Deleted - ) { - this.refresh(); - - // If we deleted the selected collection, navigate up/away - if ( - result.action === CollectionDialogAction.Deleted && - this.selectedCollection?.node.id === c?.id - ) { - void this.router.navigate([], { - queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, - queryParamsHandling: "merge", - replaceUrl: true, - }); - } - } - } - - async bulkEditCollectionAccess( - collections: CollectionView[], - organization: Organization, - ): Promise { - if (collections.length === 0) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("noCollectionsSelected"), - }); - return; - } - - if (collections.some((c) => !c.canEdit(organization))) { - this.showMissingPermissionsError(); - return; - } - - const dialog = BulkCollectionsDialogComponent.open(this.dialogService, { - data: { - collections, - organizationId: this.organization?.id, - }, - }); - - const result = await lastValueFrom(dialog.closed); - if (result === BulkCollectionsDialogResult.Saved) { - this.refresh(); - } - } - - async bulkAssignToCollections(items: CipherView[]) { - if (items.length === 0) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("nothingSelected"), - }); - return; - } - - const availableCollections = await firstValueFrom(this.editableCollections$); - - const dialog = AssignCollectionsWebComponent.open(this.dialogService, { - data: { - ciphers: items, - organizationId: this.organization?.id as OrganizationId, - availableCollections, - activeCollection: this.activeFilter?.selectedCollectionNode?.node, - isSingleCipherAdmin: - items.length === 1 && (this.organization?.canEditAllCiphers || items[0].isUnassigned), - }, - }); - - const result = await lastValueFrom(dialog.closed); - if (result === CollectionAssignmentResult.Saved) { - this.refresh(); - } - } - - async viewEvents(cipher: CipherView) { - await openEntityEventsDialog(this.dialogService, { - data: { - name: cipher.name, - organizationId: this.organization.id, - entityId: cipher.id, - showUser: true, - entity: "cipher", - }, - }); - } - - protected deleteCipherWithServer( - id: string, - userId: UserId, - permanent: boolean, - isUnassigned: boolean, - ) { - const asAdmin = this.organization?.canEditAllCiphers || isUnassigned; - return permanent - ? this.cipherService.deleteWithServer(id, userId, asAdmin) - : this.cipherService.softDeleteWithServer(id, userId, asAdmin); - } - - protected async repromptCipher(ciphers: CipherView[]) { - const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None); - - return notProtected || (await this.passwordRepromptService.showPasswordPrompt()); - } - - private refresh() { - this.refresh$.next(); - this.vaultItemsComponent?.clearSelection(); - } - - private go(queryParams: any = null) { - if (queryParams == null) { - queryParams = { - type: this.activeFilter.cipherType, - collectionId: this.activeFilter.collectionId, - deleted: this.activeFilter.isDeleted || null, - }; - } - - void this.router.navigate([], { - relativeTo: this.route, - queryParams: queryParams, - queryParamsHandling: "merge", - replaceUrl: true, - }); - } - - protected readonly CollectionDialogTabType = CollectionDialogTabType; - - private showMissingPermissionsError() { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("missingPermissions"), - }); - } -} - -/** - * Allows backwards compatibility with - * old links that used the original `cipherId` param - */ -const getCipherIdFromParams = (params: Params): string => { - return params["itemId"] || params["cipherId"]; -}; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index 1ab76c74655..3341a428970 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -9,11 +9,11 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { CipherArchiveService } from "@bitwarden/vault"; import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component"; import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-routing.module.ts b/apps/web/src/app/admin-console/organizations/collections/vault-routing.module.ts index d529c4c31fe..7ad9f050d7b 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-routing.module.ts @@ -1,26 +1,19 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { canAccessVaultTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; -import { VaultComponent } from "./deprecated_vault.component"; -import { vNextVaultComponent } from "./vault.component"; +import { VaultComponent } from "./vault.component"; const routes: Routes = [ - ...featureFlaggedRoute({ - defaultComponent: VaultComponent, - flaggedComponent: vNextVaultComponent, - featureFlag: FeatureFlag.CollectionVaultRefactor, - routeOptions: { - data: { titleId: "vaults" }, - path: "", - canActivate: [organizationPermissionsGuard(canAccessVaultTab)], - }, - }), + { + data: { titleId: "vaults" }, + path: "", + canActivate: [organizationPermissionsGuard(canAccessVaultTab)], + component: VaultComponent, + }, ]; @NgModule({ diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 64aa6936468..b961de9e24c 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -44,9 +44,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -162,7 +160,7 @@ enum AddAccessStatusType { { provide: CipherFormConfigService, useClass: AdminConsoleCipherFormConfigService }, ], }) -export class vNextVaultComponent implements OnInit, OnDestroy { +export class VaultComponent implements OnInit, OnDestroy { protected Unassigned = Unassigned; trashCleanupWarning: string = this.i18nService.t( @@ -239,7 +237,6 @@ export class vNextVaultComponent implements OnInit, OnDestroy { private totpService: TotpService, private apiService: ApiService, private toastService: ToastService, - private configService: ConfigService, private cipherFormConfigService: CipherFormConfigService, protected billingApiService: BillingApiServiceAbstraction, private accountService: AccountService, @@ -710,14 +707,13 @@ export class vNextVaultComponent implements OnInit, OnDestroy { } async navigateToPaymentMethod() { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; const organizationId = await firstValueFrom(this.organizationId$); - await this.router.navigate(["organizations", `${organizationId}`, "billing", route], { - state: { launchPaymentModalAutomatically: true }, - }); + await this.router.navigate( + ["organizations", `${organizationId}`, "billing", "payment-details"], + { + state: { launchPaymentModalAutomatically: true }, + }, + ); } addAccessToggle(e: AddAccessStatusType) { @@ -982,7 +978,7 @@ export class vNextVaultComponent implements OnInit, OnDestroy { // Allow restore of an Unassigned Item try { - if (c.id == null) { + if (c.id == null || c.id === "") { throw new Error("Cipher must have an Id to be restored"); } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); @@ -1215,7 +1211,7 @@ export class vNextVaultComponent implements OnInit, OnDestroy { aType = "Password"; value = cipher.login.password; typeI18nKey = "password"; - } else if (field === "totp") { + } else if (field === "totp" && cipher.login.totp != null) { aType = "TOTP"; const totpResponse = await firstValueFrom(this.totpService.getCode$(cipher.login.totp)); value = totpResponse.code; @@ -1236,7 +1232,7 @@ export class vNextVaultComponent implements OnInit, OnDestroy { return; } - if (!cipher.viewPassword) { + if (!cipher.viewPassword || value == null) { return; } diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.module.ts b/apps/web/src/app/admin-console/organizations/collections/vault.module.ts index 92dbc5d832c..d7c6a468eba 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.module.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.module.ts @@ -2,14 +2,12 @@ import { NgModule } from "@angular/core"; import { SharedModule } from "../../../shared/shared.module"; import { OrganizationBadgeModule } from "../../../vault/individual-vault/organization-badge/organization-badge.module"; -import { ViewComponent } from "../../../vault/individual-vault/view.component"; import { CollectionDialogComponent } from "../shared/components/collection-dialog"; import { CollectionNameBadgeComponent } from "./collection-badge"; -import { VaultComponent } from "./deprecated_vault.component"; import { GroupBadgeModule } from "./group-badge/group-badge.module"; import { VaultRoutingModule } from "./vault-routing.module"; -import { vNextVaultComponent } from "./vault.component"; +import { VaultComponent } from "./vault.component"; @NgModule({ imports: [ @@ -20,8 +18,6 @@ import { vNextVaultComponent } from "./vault.component"; OrganizationBadgeModule, CollectionDialogComponent, VaultComponent, - vNextVaultComponent, - ViewComponent, ], }) export class VaultModule {} diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 10290d52f1e..e5af0faa164 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -71,10 +71,9 @@ > - @let paymentDetailsPageData = paymentDetailsPageData$ | async; diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index a9b61debf89..b9d44c125ad 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -23,9 +23,6 @@ import { PolicyType, ProviderStatusType } from "@bitwarden/common/admin-console/ 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 { 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 { getById } from "@bitwarden/common/platform/misc"; import { BannerModule, IconModule } from "@bitwarden/components"; @@ -70,11 +67,6 @@ export class OrganizationLayoutComponent implements OnInit { protected showSponsoredFamiliesDropdown$: Observable; - protected paymentDetailsPageData$: Observable<{ - route: string; - textKey: string; - }>; - protected subscriber$: Observable; protected getTaxIdWarning$: () => Observable; @@ -82,12 +74,10 @@ export class OrganizationLayoutComponent implements OnInit { private route: ActivatedRoute, private organizationService: OrganizationService, private platformUtilsService: PlatformUtilsService, - private configService: ConfigService, private policyService: PolicyService, private providerService: ProviderService, private accountService: AccountService, private freeFamiliesPolicyService: FreeFamiliesPolicyService, - private organizationBillingService: OrganizationBillingServiceAbstraction, private organizationWarningsService: OrganizationWarningsService, ) {} @@ -141,16 +131,6 @@ export class OrganizationLayoutComponent implements OnInit { this.integrationPageEnabled$ = this.organization$.pipe(map((org) => org.canAccessIntegrations)); - this.paymentDetailsPageData$ = this.configService - .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) - .pipe( - map((managePaymentDetailsOutsideCheckout) => - managePaymentDetailsOutsideCheckout - ? { route: "billing/payment-details", textKey: "paymentDetails" } - : { route: "billing/payment-method", textKey: "paymentMethod" }, - ), - ); - this.subscriber$ = this.organization$.pipe( map((organization) => ({ type: "organization", diff --git a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts index 8484b05283d..b4c5a273ac7 100644 --- a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts @@ -28,7 +28,7 @@ import { EventService } from "../../../core"; import { SharedModule } from "../../../shared"; export interface EntityEventsDialogParams { - entity: "user" | "cipher" | "secret" | "project"; + entity: "user" | "cipher" | "secret" | "project" | "service-account"; entityId: string; organizationId?: string; @@ -174,6 +174,14 @@ export class EntityEventsComponent implements OnInit, OnDestroy { dates[1], clearExisting ? null : this.continuationToken, ); + } else if (this.params.entity === "service-account") { + response = await this.apiService.getEventsServiceAccount( + this.params.organizationId, + this.params.entityId, + dates[0], + dates[1], + clearExisting ? null : this.continuationToken, + ); } else if (this.params.entity === "project") { response = await this.apiService.getEventsProject( this.params.organizationId, diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 5a1ae6cd98b..b31f1cbf358 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -975,12 +975,11 @@ export class MembersComponent extends BaseMembersComponent } async navigateToPaymentMethod(organization: Organization) { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, + await this.router.navigate( + ["organizations", `${organization.id}`, "billing", "payment-details"], + { + state: { launchPaymentModalAutomatically: true }, + }, ); - const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; - await this.router.navigate(["organizations", `${organization.id}`, "billing", route], { - state: { launchPaymentModalAutomatically: true }, - }); } } diff --git a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts index 2e5faea4702..9293c686b7f 100644 --- a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts @@ -9,6 +9,8 @@ import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/po import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import type { PolicyEditDialogComponent } from "./policy-edit-dialog.component"; + /** * A metadata class that defines how a policy is displayed in the Admin Console Policies page for editing. * Add this to the `ossPolicyRegister` or `bitPolicyRegister` file to register it in the application. @@ -32,6 +34,13 @@ export abstract class BasePolicyEditDefinition { */ abstract component: Constructor; + /** + * The dialog component that will be opened when editing this policy. + * This allows customizing the look and feel of each policy's dialog contents. + * If not specified, defaults to {@link PolicyEditDialogComponent}. + */ + editDialogComponent?: typeof PolicyEditDialogComponent; + /** * If true, the {@link description} will be reused in the policy edit modal. Set this to false if you * have more complex requirements that you will implement in your template instead. diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.html b/apps/web/src/app/admin-console/organizations/policies/policies.component.html index ea14986749f..8df73a50e14 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.html @@ -1,7 +1,6 @@ - @let organization = organization$ | async; @if (loading) { - @for (p of policies; track p.name) { - @if (p.display$(organization, configService) | async) { - - - - @if (policiesEnabledMap.get(p.type)) { - {{ "on" | i18n }} - } - {{ p.description | i18n }} - - - } + @for (p of policies$ | async; track p.type) { + + + + @if (policiesEnabledMap.get(p.type)) { + {{ "on" | i18n }} + } + {{ p.description | i18n }} + + } diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 45383133687..95c00f74f1c 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -2,8 +2,17 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, lastValueFrom, Observable } from "rxjs"; -import { first, map } from "rxjs/operators"; +import { + combineLatest, + firstValueFrom, + lastValueFrom, + Observable, + of, + switchMap, + first, + map, + withLatestFrom, +} from "rxjs"; import { getOrganizationById, @@ -11,7 +20,6 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -39,8 +47,7 @@ import { POLICY_EDIT_REGISTER } from "./policy-register-token"; export class PoliciesComponent implements OnInit { loading = true; organizationId: string; - policies: readonly BasePolicyEditDefinition[]; - protected organization$: Observable; + policies$: Observable; private orgPolicies: PolicyResponse[]; protected policiesEnabledMap: Map = new Map(); @@ -63,28 +70,41 @@ export class PoliciesComponent implements OnInit { this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - this.organization$ = this.organizationService + const organization$ = this.organizationService .organizations$(userId) .pipe(getOrganizationById(this.organizationId)); - this.policies = this.policyListService.getPolicies(); + this.policies$ = organization$.pipe( + withLatestFrom(of(this.policyListService.getPolicies())), + switchMap(([organization, policies]) => { + return combineLatest( + policies.map((policy) => + policy + .display$(organization, this.configService) + .pipe(map((shouldDisplay) => ({ policy, shouldDisplay }))), + ), + ); + }), + map((results) => + results.filter((result) => result.shouldDisplay).map((result) => result.policy), + ), + ); await this.load(); // Handle policies component launch from Event message - this.route.queryParams - .pipe(first()) + combineLatest([this.route.queryParams.pipe(first()), this.policies$]) /* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */ - .subscribe(async (qParams) => { + .subscribe(async ([qParams, policies]) => { if (qParams.policyId != null) { const policyIdFromEvents: string = qParams.policyId; for (const orgPolicy of this.orgPolicies) { if (orgPolicy.id === policyIdFromEvents) { - for (let i = 0; i < this.policies.length; i++) { - if (this.policies[i].type === orgPolicy.type) { + for (let i = 0; i < policies.length; i++) { + if (policies[i].type === orgPolicy.type) { // 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.edit(this.policies[i]); + this.edit(policies[i]); break; } } @@ -107,7 +127,8 @@ export class PoliciesComponent implements OnInit { } async edit(policy: BasePolicyEditDefinition) { - const dialogRef = PolicyEditDialogComponent.open(this.dialogService, { + const dialogComponent = policy.editDialogComponent ?? PolicyEditDialogComponent; + const dialogRef = dialogComponent.open(this.dialogService, { data: { policy: policy, organizationId: this.organizationId, diff --git a/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts b/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts index 3073917e57b..02f870f094d 100644 --- a/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts +++ b/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts @@ -1,7 +1,16 @@ // FIXME: update to use a const object instead of a typescript enum // eslint-disable-next-line @bitwarden/platform/no-enums export enum WebauthnLoginCredentialPrfStatus { + /** + * Encrypted user key present, PRF function is supported. + */ Enabled = 0, + /** + * PRF function is supported. + */ Supported = 1, + /** + * PRF function is not supported. + */ Unsupported = 2, } diff --git a/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential.response.ts b/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential.response.ts index 85e7a7368e0..aba5940d752 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential.response.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential.response.ts @@ -40,6 +40,6 @@ export class WebauthnLoginCredentialResponse extends BaseResponse { } hasPrfKeyset(): boolean { - return this.encryptedUserKey != null && this.encryptedPublicKey != null; + return this.prfStatus === WebauthnLoginCredentialPrfStatus.Enabled; } } diff --git a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts index c2a9946ea38..74323773e66 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/webauthn-login-admin.service.spec.ts @@ -10,15 +10,19 @@ import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/ab import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { makeSymmetricCryptoKey } from "@bitwarden/common/spec"; +import { makeEncString, makeSymmetricCryptoKey } from "@bitwarden/common/spec"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; +import { UserId } from "@bitwarden/user-core"; +import { WebauthnLoginCredentialPrfStatus } from "../../enums/webauthn-login-credential-prf-status.enum"; import { CredentialCreateOptionsView } from "../../views/credential-create-options.view"; import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view"; import { RotateableKeySetService } from "../rotateable-key-set.service"; import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request"; +import { WebauthnLoginCredentialResponse } from "./response/webauthn-login-credential.response"; import { WebAuthnLoginAdminApiService } from "./webauthn-login-admin-api.service"; import { WebauthnLoginAdminService } from "./webauthn-login-admin.service"; @@ -248,6 +252,79 @@ describe("WebauthnAdminService", () => { expect(rotateKeySetMock).not.toHaveBeenCalled(); }); }); + + describe("getRotatedData", () => { + const mockRotatedPublicKey = makeEncString("rotated_encryptedPublicKey"); + const mockRotatedUserKey = makeEncString("rotated_encryptedUserKey"); + const oldUserKey = makeSymmetricCryptoKey(64) as UserKey; + const newUserKey = makeSymmetricCryptoKey(64) as UserKey; + const userId = Utils.newGuid() as UserId; + + it("should only include credentials with PRF keysets", async () => { + const responseUnsupported = new WebauthnLoginCredentialResponse({ + id: "test-credential-id-1", + name: "Test Credential 1", + prfStatus: WebauthnLoginCredentialPrfStatus.Unsupported, + encryptedPublicKey: null, + encryptedUserKey: null, + }); + const responseSupported = new WebauthnLoginCredentialResponse({ + id: "test-credential-id-2", + name: "Test Credential 2", + prfStatus: WebauthnLoginCredentialPrfStatus.Supported, + encryptedPublicKey: null, + encryptedUserKey: null, + }); + const responseEnabled = new WebauthnLoginCredentialResponse({ + id: "test-credential-id-3", + name: "Test Credential 3", + prfStatus: WebauthnLoginCredentialPrfStatus.Enabled, + encryptedPublicKey: makeEncString("encryptedPublicKey").toJSON(), + encryptedUserKey: makeEncString("encryptedUserKey").toJSON(), + }); + + apiService.getCredentials.mockResolvedValue( + new ListResponse( + { + data: [responseUnsupported, responseSupported, responseEnabled], + }, + WebauthnLoginCredentialResponse, + ), + ); + + rotateableKeySetService.rotateKeySet.mockResolvedValue( + new RotateableKeySet(mockRotatedUserKey, mockRotatedPublicKey), + ); + + const result = await service.getRotatedData(oldUserKey, newUserKey, userId); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual( + expect.objectContaining({ + id: "test-credential-id-3", + encryptedPublicKey: mockRotatedPublicKey, + encryptedUserKey: mockRotatedUserKey, + }), + ); + expect(rotateableKeySetService.rotateKeySet).toHaveBeenCalledTimes(1); + expect(rotateableKeySetService.rotateKeySet).toHaveBeenCalledWith( + responseEnabled.getRotateableKeyset(), + oldUserKey, + newUserKey, + ); + }); + + it("should error when getCredentials fails", async () => { + const expectedError = "API connection failed"; + apiService.getCredentials.mockRejectedValue(new Error(expectedError)); + + await expect(service.getRotatedData(oldUserKey, newUserKey, userId)).rejects.toThrow( + expectedError, + ); + + expect(rotateableKeySetService.rotateKeySet).not.toHaveBeenCalled(); + }); + }); }); function createCredentialCreateOptions(): CredentialCreateOptionsView { diff --git a/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.html b/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.html deleted file mode 100644 index 94dfac42976..00000000000 --- a/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.html +++ /dev/null @@ -1,54 +0,0 @@ -
-
- -

- {{ "readingPasskeyLoading" | i18n }} -

- -
-
- -
- -
-

{{ "readingPasskeyLoadingInfo" | i18n }}

- -
- - -
- -
-

{{ "readingPasskeyLoadingInfo" | i18n }}

- -
-
-

- {{ "troubleLoggingIn" | i18n }}
- {{ "useADifferentLogInMethod" | i18n }} -

-
-
-
diff --git a/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts b/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts deleted file mode 100644 index 695e935b919..00000000000 --- a/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Component } from "@angular/core"; - -import { BaseLoginViaWebAuthnComponent } from "@bitwarden/angular/auth/components/base-login-via-webauthn.component"; -import { - TwoFactorAuthSecurityKeyIcon, - TwoFactorAuthSecurityKeyFailedIcon, -} from "@bitwarden/assets/svg"; - -@Component({ - selector: "app-login-via-webauthn", - templateUrl: "login-via-webauthn.component.html", - standalone: false, -}) -export class LoginViaWebAuthnComponent extends BaseLoginViaWebAuthnComponent { - protected readonly Icons = { - TwoFactorAuthSecurityKeyIcon, - TwoFactorAuthSecurityKeyFailedIcon, - }; -} diff --git a/apps/web/src/app/auth/login/login.module.ts b/apps/web/src/app/auth/login/login.module.ts deleted file mode 100644 index 9a99c84f727..00000000000 --- a/apps/web/src/app/auth/login/login.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NgModule } from "@angular/core"; - -import { CheckboxModule } from "@bitwarden/components"; - -import { SharedModule } from "../../../app/shared"; - -import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component"; - -@NgModule({ - imports: [SharedModule, CheckboxModule], - declarations: [LoginViaWebAuthnComponent], - exports: [LoginViaWebAuthnComponent], -}) -export class LoginModule {} 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 deleted file mode 100644 index 0c1a4270662..00000000000 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html +++ /dev/null @@ -1,104 +0,0 @@ - - - {{ "loading" | i18n }} - -
-
-
-

{{ "billingPlanLabel" | i18n }}

-
- -
-
- -
-
-
-

{{ "paymentType" | i18n }}

- - - - @if (trialLength === 0) { - @let priceLabel = - subscriptionProduct === SubscriptionProduct.PasswordManager - ? "passwordManagerPlanPrice" - : "secretsManagerPlanPrice"; - -
-
- {{ priceLabel | i18n }}: {{ getPriceFor(formGroup.value.cadence) | currency: "USD $" }} -
- {{ "estimatedTax" | i18n }}: - @if (fetchingTaxAmount) { - - } @else { - {{ taxAmount | currency: "USD $" }} - } -
-
-
-

- {{ "total" | i18n }}: - @if (fetchingTaxAmount) { - - } @else { - {{ total | currency: "USD $" }}/{{ interval | i18n }} - } -

-
- } -
-
- - -
-
-
- - - - {{ "loading" | i18n }} - diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts deleted file mode 100644 index 431f8882505..00000000000 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ /dev/null @@ -1,360 +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, - ViewChild, -} from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { firstValueFrom, from, Subject, switchMap, takeUntil } from "rxjs"; - -import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { - BillingInformation, - OrganizationBillingServiceAbstraction as OrganizationBillingService, - OrganizationInformation, - PaymentInformation, - PlanInformation, -} from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { - PaymentMethodType, - PlanType, - ProductTierType, - ProductType, -} from "@bitwarden/common/billing/enums"; -import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax"; -import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { ToastService } from "@bitwarden/components"; - -import { BillingSharedModule } from "../../shared"; -import { PaymentComponent } from "../../shared/payment/payment.component"; - -export type TrialOrganizationType = Exclude; - -export interface OrganizationInfo { - name: string; - email: string; - type: TrialOrganizationType | null; -} - -export interface OrganizationCreatedEvent { - organizationId: string; - planDescription: string; -} - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -enum SubscriptionCadence { - Annual, - Monthly, -} - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum SubscriptionProduct { - PasswordManager, - SecretsManager, -} - -@Component({ - selector: "app-trial-billing-step", - templateUrl: "trial-billing-step.component.html", - imports: [BillingSharedModule], -}) -export class TrialBillingStepComponent implements OnInit, OnDestroy { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent; - @Input() organizationInfo: OrganizationInfo; - @Input() subscriptionProduct: SubscriptionProduct = SubscriptionProduct.PasswordManager; - @Input() trialLength: number; - @Output() steppedBack = new EventEmitter(); - @Output() organizationCreated = new EventEmitter(); - - loading = true; - fetchingTaxAmount = false; - - annualCadence = SubscriptionCadence.Annual; - monthlyCadence = SubscriptionCadence.Monthly; - - formGroup = this.formBuilder.group({ - cadence: [SubscriptionCadence.Annual, Validators.required], - }); - formPromise: Promise; - - applicablePlans: PlanResponse[]; - annualPlan?: PlanResponse; - monthlyPlan?: PlanResponse; - - taxAmount = 0; - - private destroy$ = new Subject(); - - protected readonly SubscriptionProduct = SubscriptionProduct; - - constructor( - private apiService: ApiService, - private i18nService: I18nService, - private formBuilder: FormBuilder, - private messagingService: MessagingService, - private organizationBillingService: OrganizationBillingService, - private toastService: ToastService, - private taxService: TaxServiceAbstraction, - private accountService: AccountService, - ) {} - - async ngOnInit(): Promise { - const plans = await this.apiService.getPlans(); - this.applicablePlans = plans.data.filter(this.isApplicable); - this.annualPlan = this.findPlanFor(SubscriptionCadence.Annual); - this.monthlyPlan = this.findPlanFor(SubscriptionCadence.Monthly); - - if (this.trialLength === 0) { - this.formGroup.controls.cadence.valueChanges - .pipe( - switchMap((cadence) => from(this.previewTaxAmount(cadence))), - takeUntil(this.destroy$), - ) - .subscribe((taxAmount) => { - this.taxAmount = taxAmount; - }); - } - - this.loading = false; - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - async submit(): Promise { - if (!this.taxInfoComponent.validate()) { - return; - } - - this.formPromise = this.createOrganization(); - - const organizationId = await this.formPromise; - const planDescription = this.getPlanDescription(); - - this.toastService.showToast({ - variant: "success", - title: this.i18nService.t("organizationCreated"), - message: this.i18nService.t("organizationReadyToGo"), - }); - - this.organizationCreated.emit({ - organizationId, - planDescription, - }); - - // TODO: No one actually listening to this? - this.messagingService.send("organizationCreated", { organizationId }); - } - - async onTaxInformationChanged() { - if (this.trialLength === 0) { - this.taxAmount = await this.previewTaxAmount(this.formGroup.value.cadence); - } - - this.paymentComponent.showBankAccount = - this.taxInfoComponent.getTaxInformation().country === "US"; - if ( - !this.paymentComponent.showBankAccount && - this.paymentComponent.selected === PaymentMethodType.BankAccount - ) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - - protected getPriceFor(cadence: SubscriptionCadence): number { - const plan = this.findPlanFor(cadence); - return this.subscriptionProduct === SubscriptionProduct.PasswordManager - ? plan.PasswordManager.basePrice === 0 - ? plan.PasswordManager.seatPrice - : plan.PasswordManager.basePrice - : plan.SecretsManager.basePrice === 0 - ? plan.SecretsManager.seatPrice - : plan.SecretsManager.basePrice; - } - - protected stepBack() { - this.steppedBack.emit(); - } - - private async createOrganization(): Promise { - const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - const planResponse = this.findPlanFor(this.formGroup.value.cadence); - - const { type, token } = await this.paymentComponent.tokenize(); - const paymentMethod: [string, PaymentMethodType] = [token, type]; - - const organization: OrganizationInformation = { - name: this.organizationInfo.name, - billingEmail: this.organizationInfo.email, - initiationPath: - this.subscriptionProduct === SubscriptionProduct.PasswordManager - ? "Password Manager trial from marketing website" - : "Secrets Manager trial from marketing website", - }; - - const plan: PlanInformation = { - type: planResponse.type, - passwordManagerSeats: 1, - }; - - if (this.subscriptionProduct === SubscriptionProduct.SecretsManager) { - plan.subscribeToSecretsManager = true; - plan.isFromSecretsManagerTrial = true; - plan.secretsManagerSeats = 1; - } - - const payment: PaymentInformation = { - paymentMethod, - billing: this.getBillingInformationFromTaxInfoComponent(), - skipTrial: this.trialLength === 0, - }; - - const response = await this.organizationBillingService.purchaseSubscription( - { - organization, - plan, - payment, - }, - activeUserId, - ); - - return response.id; - } - - private productTypeToPlanTypeMap: { - [productType in TrialOrganizationType]: { - [cadence in SubscriptionCadence]?: PlanType; - }; - } = { - [ProductTierType.Enterprise]: { - [SubscriptionCadence.Annual]: PlanType.EnterpriseAnnually, - [SubscriptionCadence.Monthly]: PlanType.EnterpriseMonthly, - }, - [ProductTierType.Families]: { - [SubscriptionCadence.Annual]: PlanType.FamiliesAnnually, - // No monthly option for Families plan - }, - [ProductTierType.Teams]: { - [SubscriptionCadence.Annual]: PlanType.TeamsAnnually, - [SubscriptionCadence.Monthly]: PlanType.TeamsMonthly, - }, - [ProductTierType.TeamsStarter]: { - // No annual option for Teams Starter plan - [SubscriptionCadence.Monthly]: PlanType.TeamsStarter, - }, - }; - - private findPlanFor(cadence: SubscriptionCadence): PlanResponse | null { - const productType = this.organizationInfo.type; - const planType = this.productTypeToPlanTypeMap[productType]?.[cadence]; - return planType ? this.applicablePlans.find((plan) => plan.type === planType) : null; - } - - protected get showTaxIdField(): boolean { - switch (this.organizationInfo.type) { - case ProductTierType.Families: - return false; - default: - return true; - } - } - - private getBillingInformationFromTaxInfoComponent(): BillingInformation { - return { - postalCode: this.taxInfoComponent.getTaxInformation()?.postalCode, - country: this.taxInfoComponent.getTaxInformation()?.country, - taxId: this.taxInfoComponent.getTaxInformation()?.taxId, - addressLine1: this.taxInfoComponent.getTaxInformation()?.line1, - addressLine2: this.taxInfoComponent.getTaxInformation()?.line2, - city: this.taxInfoComponent.getTaxInformation()?.city, - state: this.taxInfoComponent.getTaxInformation()?.state, - }; - } - - private getPlanDescription(): string { - const plan = this.findPlanFor(this.formGroup.value.cadence); - const price = - this.subscriptionProduct === SubscriptionProduct.PasswordManager - ? plan.PasswordManager.basePrice === 0 - ? plan.PasswordManager.seatPrice - : plan.PasswordManager.basePrice - : plan.SecretsManager.basePrice === 0 - ? plan.SecretsManager.seatPrice - : plan.SecretsManager.basePrice; - - switch (this.formGroup.value.cadence) { - case SubscriptionCadence.Annual: - return `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`; - case SubscriptionCadence.Monthly: - return `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`; - } - } - - private isApplicable(plan: PlanResponse): boolean { - const hasCorrectProductType = - plan.productTier === ProductTierType.Enterprise || - plan.productTier === ProductTierType.Families || - plan.productTier === ProductTierType.Teams || - plan.productTier === ProductTierType.TeamsStarter; - const notDisabledOrLegacy = !plan.disabled && !plan.legacyYear; - return hasCorrectProductType && notDisabledOrLegacy; - } - - private previewTaxAmount = async (cadence: SubscriptionCadence): Promise => { - this.fetchingTaxAmount = true; - - if (!this.taxInfoComponent.validate()) { - this.fetchingTaxAmount = false; - return 0; - } - - const plan = this.findPlanFor(cadence); - - const productType = - this.subscriptionProduct === SubscriptionProduct.PasswordManager - ? ProductType.PasswordManager - : ProductType.SecretsManager; - - const taxInformation = this.taxInfoComponent.getTaxInformation(); - - const request: PreviewTaxAmountForOrganizationTrialRequest = { - planType: plan.type, - productType, - taxInformation: { - ...taxInformation, - }, - }; - - const response = await this.taxService.previewTaxAmountForOrganizationTrial(request); - this.fetchingTaxAmount = false; - return response; - }; - - get price() { - return this.getPriceFor(this.formGroup.value.cadence); - } - - get total() { - return this.price + this.taxAmount; - } - - get interval() { - return this.formGroup.value.cadence === SubscriptionCadence.Annual ? "year" : "month"; - } -} diff --git a/apps/web/src/app/billing/clients/index.ts b/apps/web/src/app/billing/clients/index.ts index ff962abcbf3..17f64248cfa 100644 --- a/apps/web/src/app/billing/clients/index.ts +++ b/apps/web/src/app/billing/clients/index.ts @@ -1,2 +1,3 @@ export * from "./organization-billing.client"; export * from "./subscriber-billing.client"; +export * from "./tax.client"; diff --git a/apps/web/src/app/billing/clients/subscriber-billing.client.ts b/apps/web/src/app/billing/clients/subscriber-billing.client.ts index 18ca215ef0c..107a8ccc728 100644 --- a/apps/web/src/app/billing/clients/subscriber-billing.client.ts +++ b/apps/web/src/app/billing/clients/subscriber-billing.client.ts @@ -82,6 +82,24 @@ export class SubscriberBillingClient { return data ? new MaskedPaymentMethodResponse(data).value : null; }; + restartSubscription = async ( + subscriber: BitwardenSubscriber, + paymentMethod: TokenizedPaymentMethod, + billingAddress: BillingAddress, + ): Promise => { + const path = `${this.getEndpoint(subscriber)}/subscription/restart`; + await this.apiService.send( + "POST", + path, + { + paymentMethod, + billingAddress, + }, + true, + false, + ); + }; + updateBillingAddress = async ( subscriber: BitwardenSubscriber, billingAddress: BillingAddress, diff --git a/apps/web/src/app/billing/clients/tax.client.ts b/apps/web/src/app/billing/clients/tax.client.ts new file mode 100644 index 00000000000..09debd5a210 --- /dev/null +++ b/apps/web/src/app/billing/clients/tax.client.ts @@ -0,0 +1,131 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BillingAddress } from "@bitwarden/web-vault/app/billing/payment/types"; + +class TaxAmountResponse extends BaseResponse implements TaxAmounts { + tax: number; + total: number; + + constructor(response: any) { + super(response); + + this.tax = this.getResponseProperty("Tax"); + this.total = this.getResponseProperty("Total"); + } +} + +export type OrganizationSubscriptionPlan = { + tier: "families" | "teams" | "enterprise"; + cadence: "annually" | "monthly"; +}; + +export type OrganizationSubscriptionPurchase = OrganizationSubscriptionPlan & { + passwordManager: { + seats: number; + additionalStorage: number; + sponsored: boolean; + }; + secretsManager?: { + seats: number; + additionalServiceAccounts: number; + standalone: boolean; + }; +}; + +export type OrganizationSubscriptionUpdate = { + passwordManager?: { + seats?: number; + additionalStorage?: number; + }; + secretsManager?: { + seats?: number; + additionalServiceAccounts?: number; + }; +}; + +export interface TaxAmounts { + tax: number; + total: number; +} + +@Injectable() +export class TaxClient { + constructor(private apiService: ApiService) {} + + previewTaxForOrganizationSubscriptionPurchase = async ( + purchase: OrganizationSubscriptionPurchase, + billingAddress: BillingAddress, + ): Promise => { + const json = await this.apiService.send( + "POST", + "/billing/tax/organizations/subscriptions/purchase", + { + purchase, + billingAddress, + }, + true, + true, + ); + + return new TaxAmountResponse(json); + }; + + previewTaxForOrganizationSubscriptionPlanChange = async ( + organizationId: string, + plan: { + tier: "families" | "teams" | "enterprise"; + cadence: "annually" | "monthly"; + }, + billingAddress: BillingAddress | null, + ): Promise => { + const json = await this.apiService.send( + "POST", + `/billing/tax/organizations/${organizationId}/subscription/plan-change`, + { + plan, + billingAddress, + }, + true, + true, + ); + + return new TaxAmountResponse(json); + }; + + previewTaxForOrganizationSubscriptionUpdate = async ( + organizationId: string, + update: OrganizationSubscriptionUpdate, + ): Promise => { + const json = await this.apiService.send( + "POST", + `/billing/tax/organizations/${organizationId}/subscription/update`, + { + update, + }, + true, + true, + ); + + return new TaxAmountResponse(json); + }; + + previewTaxForPremiumSubscriptionPurchase = async ( + additionalStorage: number, + billingAddress: BillingAddress, + ): Promise => { + const json = await this.apiService.send( + "POST", + `/billing/tax/premium/subscriptions/purchase`, + { + additionalStorage, + billingAddress, + }, + true, + true, + ); + + return new TaxAmountResponse(json); + }; +} diff --git a/apps/web/src/app/billing/index.ts b/apps/web/src/app/billing/index.ts index 217f1e05be9..a3047bbab6a 100644 --- a/apps/web/src/app/billing/index.ts +++ b/apps/web/src/app/billing/index.ts @@ -1,2 +1 @@ export { OrganizationPlansComponent } from "./organizations"; -export { TaxInfoComponent } from "./shared"; diff --git a/apps/web/src/app/billing/individual/index.ts b/apps/web/src/app/billing/individual/index.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts index 87b342ed997..bb0ca60b677 100644 --- a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts @@ -3,8 +3,6 @@ import { RouterModule, Routes } from "@angular/router"; import { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component"; -import { PaymentMethodComponent } from "../shared"; - import { BillingHistoryViewComponent } from "./billing-history-view.component"; import { PremiumComponent } from "./premium/premium.component"; import { SubscriptionComponent } from "./subscription.component"; @@ -27,11 +25,6 @@ const routes: Routes = [ component: PremiumComponent, data: { titleId: "goPremium" }, }, - { - path: "payment-method", - component: PaymentMethodComponent, - data: { titleId: "paymentMethod" }, - }, { path: "payment-details", component: AccountPaymentDetailsComponent, diff --git a/apps/web/src/app/billing/individual/individual-billing.module.ts b/apps/web/src/app/billing/individual/individual-billing.module.ts index ad75da00c99..20f2a6cc143 100644 --- a/apps/web/src/app/billing/individual/individual-billing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing.module.ts @@ -1,5 +1,10 @@ import { NgModule } from "@angular/core"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, +} from "@bitwarden/web-vault/app/billing/payment/components"; + import { HeaderModule } from "../../layouts/header/header.module"; import { BillingSharedModule } from "../shared"; @@ -10,7 +15,13 @@ import { SubscriptionComponent } from "./subscription.component"; import { UserSubscriptionComponent } from "./user-subscription.component"; @NgModule({ - imports: [IndividualBillingRoutingModule, BillingSharedModule, HeaderModule], + imports: [ + IndividualBillingRoutingModule, + BillingSharedModule, + HeaderModule, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, + ], declarations: [ SubscriptionComponent, BillingHistoryViewComponent, diff --git a/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts b/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts index 9f46d9d3909..ca7902542de 100644 --- a/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts +++ b/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts @@ -1,22 +1,7 @@ import { Component } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { - BehaviorSubject, - EMPTY, - filter, - from, - map, - merge, - Observable, - shareReplay, - switchMap, - tap, -} from "rxjs"; -import { catchError } from "rxjs/operators"; +import { BehaviorSubject, filter, merge, Observable, shareReplay, switchMap, tap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; @@ -28,13 +13,6 @@ import { import { MaskedPaymentMethod } from "../../payment/types"; import { mapAccountToSubscriber, BitwardenSubscriber } from "../../types"; -class RedirectError { - constructor( - public path: string[], - public relativeTo: ActivatedRoute, - ) {} -} - type View = { account: BitwardenSubscriber; paymentMethod: MaskedPaymentMethod | null; @@ -56,23 +34,11 @@ export class AccountPaymentDetailsComponent { private viewState$ = new BehaviorSubject(null); private load$: Observable = this.accountService.activeAccount$.pipe( - switchMap((account) => - this.configService - .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) - .pipe( - map((managePaymentDetailsOutsideCheckout) => { - if (!managePaymentDetailsOutsideCheckout) { - throw new RedirectError(["../payment-method"], this.activatedRoute); - } - return account; - }), - ), - ), mapAccountToSubscriber, switchMap(async (account) => { const [paymentMethod, credit] = await Promise.all([ - this.billingClient.getPaymentMethod(account), - this.billingClient.getCredit(account), + this.subscriberBillingClient.getPaymentMethod(account), + this.subscriberBillingClient.getCredit(account), ]); return { @@ -82,14 +48,6 @@ export class AccountPaymentDetailsComponent { }; }), shareReplay({ bufferSize: 1, refCount: false }), - catchError((error: unknown) => { - if (error instanceof RedirectError) { - return from(this.router.navigate(error.path, { relativeTo: error.relativeTo })).pipe( - switchMap(() => EMPTY), - ); - } - throw error; - }), ); view$: Observable = merge( @@ -99,10 +57,7 @@ export class AccountPaymentDetailsComponent { constructor( private accountService: AccountService, - private activatedRoute: ActivatedRoute, - private billingClient: SubscriberBillingClient, - private configService: ConfigService, - private router: Router, + private subscriberBillingClient: SubscriberBillingClient, ) {} setPaymentMethod = (paymentMethod: MaskedPaymentMethod) => { diff --git a/apps/web/src/app/billing/individual/premium/premium.component.html b/apps/web/src/app/billing/individual/premium/premium.component.html index 3f0f97541df..52ebe7803df 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.html +++ b/apps/web/src/app/billing/individual/premium/premium.component.html @@ -70,7 +70,7 @@ (onLicenseFileUploaded)="onLicenseFileSelectedChanged()" /> -
+

{{ "addons" | i18n }}

@@ -93,15 +93,25 @@

{{ "summary" | i18n }}

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

{{ "paymentInformation" | i18n }}

- - +
+ + + + +
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} 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 974c22455ff..d5062e34881 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -9,36 +9,34 @@ 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"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; -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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { ToastService } from "@bitwarden/components"; - -import { PaymentComponent } from "../../shared/payment/payment.component"; -import { TaxInfoComponent } from "../../shared/tax-info.component"; +import { TaxClient } from "@bitwarden/web-vault/app/billing/clients"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, + getBillingAddressFromForm, +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { tokenizablePaymentMethodToLegacyEnum } from "@bitwarden/web-vault/app/billing/payment/types"; @Component({ templateUrl: "./premium.component.html", standalone: false, + providers: [TaxClient], }) export class PremiumComponent { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; + @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; protected hasPremiumFromAnyOrganization$: Observable; - protected addOnFormGroup = new FormGroup({ + protected formGroup = new FormGroup({ additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]), - }); - - protected licenseFormGroup = new FormGroup({ - file: new FormControl(null, [Validators.required]), + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), }); protected cloudWebVaultURL: string; @@ -53,16 +51,14 @@ export class PremiumComponent { private activatedRoute: ActivatedRoute, private apiService: ApiService, private billingAccountProfileStateService: BillingAccountProfileStateService, - private configService: ConfigService, private environmentService: EnvironmentService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private router: Router, private syncService: SyncService, private toastService: ToastService, - private tokenService: TokenService, - private taxService: TaxServiceAbstraction, private accountService: AccountService, + private taxClient: TaxClient, ) { this.isSelfHost = this.platformUtilsService.isSelfHost(); @@ -93,11 +89,13 @@ export class PremiumComponent { ) .subscribe(); - this.addOnFormGroup.controls.additionalStorage.valueChanges - .pipe(debounceTime(1000), takeUntilDestroyed()) - .subscribe(() => { - this.refreshSalesTax(); - }); + this.formGroup.valueChanges + .pipe( + debounceTime(1000), + switchMap(async () => await this.refreshSalesTax()), + takeUntilDestroyed(), + ) + .subscribe(); } finalizeUpgrade = async () => { @@ -117,53 +115,21 @@ export class PremiumComponent { 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) { + if (this.formGroup.invalid) { return; } - const { type, token } = await this.paymentComponent.tokenize(); + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + + const legacyEnum = tokenizablePaymentMethodToLegacyEnum(paymentMethod.type); 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); + formData.append("paymentMethodType", legacyEnum.toString()); + formData.append("paymentToken", paymentMethod.token); + formData.append("additionalStorageGb", this.formGroup.value.additionalStorage.toString()); + formData.append("country", this.formGroup.value.billingAddress.country); + formData.append("postalCode", this.formGroup.value.billingAddress.postalCode); await this.apiService.postPremium(formData); await this.finalizeUpgrade(); @@ -171,7 +137,7 @@ export class PremiumComponent { }; protected get additionalStorageCost(): number { - return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage; + return this.storageGBPrice * this.formGroup.value.additionalStorage; } protected get premiumURL(): string { @@ -190,35 +156,18 @@ export class PremiumComponent { await this.postFinalizeUpgrade(); } - private refreshSalesTax(): void { - if (!this.taxInfoComponent.country || !this.taxInfoComponent.postalCode) { + private async refreshSalesTax(): Promise { + if (this.formGroup.invalid) { return; } - const request: PreviewIndividualInvoiceRequest = { - passwordManager: { - additionalStorage: this.addOnFormGroup.value.additionalStorage, - }, - taxInformation: { - postalCode: this.taxInfoComponent.postalCode, - country: this.taxInfoComponent.country, - }, - }; - this.taxService - .previewIndividualInvoice(request) - .then((invoice) => { - this.estimatedTax = invoice.taxAmount; - }) - .catch((error) => { - this.toastService.showToast({ - title: "", - variant: "error", - message: this.i18nService.t(error.message), - }); - }); - } + const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); - protected onTaxInformationChanged(): void { - this.refreshSalesTax(); + const taxAmounts = await this.taxClient.previewTaxForPremiumSubscriptionPurchase( + this.formGroup.value.additionalStorage, + billingAddress, + ); + + this.estimatedTax = taxAmounts.tax; } } diff --git a/apps/web/src/app/billing/individual/subscription.component.html b/apps/web/src/app/billing/individual/subscription.component.html index fa2eb0412a9..f9a46cf56ad 100644 --- a/apps/web/src/app/billing/individual/subscription.component.html +++ b/apps/web/src/app/billing/individual/subscription.component.html @@ -3,10 +3,7 @@ {{ "subscription" | i18n }} - @let paymentMethodPageData = paymentDetailsPageData$ | async; - {{ - paymentMethodPageData.textKey | i18n - }} + {{ "paymentDetails" | i18n }} {{ "billingHistory" | i18n }} diff --git a/apps/web/src/app/billing/individual/subscription.component.ts b/apps/web/src/app/billing/individual/subscription.component.ts index c6a20a9f6a3..2a08ec85127 100644 --- a/apps/web/src/app/billing/individual/subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription.component.ts @@ -1,12 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { map, Observable, switchMap } from "rxjs"; +import { Observable, switchMap } from "rxjs"; 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @Component({ @@ -15,32 +13,16 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl }) export class SubscriptionComponent implements OnInit { hasPremium$: Observable; - paymentDetailsPageData$: Observable<{ - route: string; - textKey: string; - }>; - selfHosted: boolean; constructor( private platformUtilsService: PlatformUtilsService, billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, - private configService: ConfigService, ) { this.hasPremium$ = accountService.activeAccount$.pipe( switchMap((account) => billingAccountProfileStateService.hasPremiumPersonally$(account.id)), ); - - this.paymentDetailsPageData$ = this.configService - .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) - .pipe( - map((managePaymentDetailsOutsideCheckout) => - managePaymentDetailsOutsideCheckout - ? { route: "payment-details", textKey: "paymentDetails" } - : { route: "payment-method", textKey: "paymentMethod" }, - ), - ); } ngOnInit() { 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 f899b8eccb4..abd7bdb155a 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 @@ -328,24 +328,60 @@ *ngIf="formGroup.value.productTier !== productTypes.Free || isSubscriptionCanceled" >

{{ "paymentMethod" | i18n }}

-

- - {{ paymentSource?.description }} - - {{ "changePaymentMethod" | i18n }} - +

+ @switch (paymentMethod.type) { + @case ("bankAccount") { + + {{ paymentMethod.bankName }}, *{{ paymentMethod.last4 }} + @if (paymentMethod.hostedVerificationUrl) { + - {{ "unverified" | i18n }} + } + + {{ "changePaymentMethod" | i18n }} + + } + @case ("card") { +

+ @let cardBrandIcon = getCardBrandIcon(); + @if (cardBrandIcon !== null) { + + } @else { + + } + {{ paymentMethod.brand | titlecase }}, *{{ paymentMethod.last4 }}, + {{ paymentMethod.expiration }} + + {{ "changePaymentMethod" | i18n }} + +

+ } + @case ("payPal") { + + {{ paymentMethod.email }} + + {{ "changePaymentMethod" | i18n }} + + } + }

- - + + + +

diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 6fc2dc57ba2..2b5c27e0f09 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -12,9 +12,9 @@ import { } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { firstValueFrom, map, Subject, switchMap, takeUntil } from "rxjs"; +import { combineLatest, firstValueFrom, map, Subject, switchMap, takeUntil } from "rxjs"; +import { debounceTime } 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 { @@ -28,28 +28,8 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/ import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { - BillingApiServiceAbstraction, - BillingInformation, - OrganizationBillingServiceAbstraction as OrganizationBillingService, - OrganizationInformation, - PaymentInformation, - PlanInformation, -} from "@bitwarden/common/billing/abstractions"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { - PaymentMethodType, - PlanInterval, - 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 { 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 { PlanInterval, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -57,6 +37,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { OrganizationId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { + CardComponent, DIALOG_DATA, DialogConfig, DialogRef, @@ -64,11 +45,25 @@ import { ToastService, } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { UserId } from "@bitwarden/user-core"; +import { + OrganizationSubscriptionPlan, + SubscriberBillingClient, + TaxClient, +} from "@bitwarden/web-vault/app/billing/clients"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, + getBillingAddressFromForm, +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { + BillingAddress, + getCardBrandIcon, + MaskedPaymentMethod, +} from "@bitwarden/web-vault/app/billing/payment/types"; +import { BitwardenSubscriber } from "@bitwarden/web-vault/app/billing/types"; import { BillingNotificationService } from "../services/billing-notification.service"; import { BillingSharedModule } from "../shared/billing-shared.module"; -import { PaymentComponent } from "../shared/payment/payment.component"; type ChangePlanDialogParams = { organizationId: string; @@ -111,11 +106,16 @@ interface OnSuccessArgs { @Component({ templateUrl: "./change-plan-dialog.component.html", - imports: [BillingSharedModule], + imports: [ + BillingSharedModule, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, + CardComponent, + ], + providers: [SubscriberBillingClient, TaxClient], }) export class ChangePlanDialogComponent implements OnInit, OnDestroy { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent; + @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent: EnterPaymentMethodComponent; @Input() acceptingSponsorship = false; @Input() organizationId: string; @@ -172,7 +172,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { clientOwnerEmail: ["", [Validators.email]], plan: [this.plan], productTier: [this.productTier], - // planInterval: [1], + }); + + billingFormGroup = this.formBuilder.group({ + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), }); planType: string; @@ -183,7 +187,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { secretsManagerPlans: PlanResponse[]; organization: Organization; sub: OrganizationSubscriptionResponse; - billing: BillingResponse; dialogHeaderName: string; currentPlanName: string; showPayment: boolean = false; @@ -191,15 +194,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { currentPlan: PlanResponse; isCardStateDisabled = false; focusedIndex: number | null = null; - accountCredit: number; - paymentSource?: PaymentSourceResponse; plans: ListResponse; isSubscriptionCanceled: boolean = false; secretsManagerTotal: number; - private destroy$ = new Subject(); + paymentMethod: MaskedPaymentMethod | null; + billingAddress: BillingAddress | null; - protected taxInformation: TaxInformation; + private destroy$ = new Subject(); constructor( @Inject(DIALOG_DATA) private dialogParams: ChangePlanDialogParams, @@ -215,11 +217,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private messagingService: MessagingService, private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, - private billingApiService: BillingApiServiceAbstraction, - private taxService: TaxServiceAbstraction, private accountService: AccountService, - private organizationBillingService: OrganizationBillingService, private billingNotificationService: BillingNotificationService, + private subscriberBillingClient: SubscriberBillingClient, + private taxClient: TaxClient, ) {} async ngOnInit(): Promise { @@ -242,10 +243,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ); if (this.sub?.subscription?.status !== "canceled") { try { - const { accountCredit, paymentSource } = - await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); - this.accountCredit = accountCredit; - this.paymentSource = paymentSource; + const subscriber: BitwardenSubscriber = { type: "organization", data: this.organization }; + const [paymentMethod, billingAddress] = await Promise.all([ + this.subscriberBillingClient.getPaymentMethod(subscriber), + this.subscriberBillingClient.getBillingAddress(subscriber), + ]); + + this.paymentMethod = paymentMethod; + this.billingAddress = billingAddress; } catch (error) { this.billingNotificationService.handleError(error); } @@ -307,15 +312,24 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ? 0 : (this.sub?.customerDiscount?.percentOff ?? 0); - this.setInitialPlanSelection(); - this.loading = false; - - const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); - this.taxInformation = TaxInformation.from(taxInfo); - + await this.setInitialPlanSelection(); if (!this.isSubscriptionCanceled) { - this.refreshSalesTax(); + await this.refreshSalesTax(); } + + combineLatest([ + this.billingFormGroup.controls.billingAddress.controls.country.valueChanges, + this.billingFormGroup.controls.billingAddress.controls.postalCode.valueChanges, + this.billingFormGroup.controls.billingAddress.controls.taxId.valueChanges, + ]) + .pipe( + debounceTime(1000), + switchMap(async () => await this.refreshSalesTax()), + takeUntil(this.destroy$), + ) + .subscribe(); + + this.loading = false; } resolveHeaderName(subscription: OrganizationSubscriptionResponse): string { @@ -333,10 +347,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ); } - setInitialPlanSelection() { + async setInitialPlanSelection() { this.focusedIndex = this.selectableProducts.length - 1; if (!this.isSubscriptionCanceled) { - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + await this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); } } @@ -344,10 +358,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return this.selectableProducts.find((product) => product.productTier === productTier); } - isPaymentSourceEmpty() { - return this.paymentSource === null || this.paymentSource === undefined; - } - isSecretsManagerTrial(): boolean { return ( this.sub?.subscription?.items?.some((item) => @@ -356,13 +366,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ); } - planTypeChanged() { - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + async planTypeChanged() { + await this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); } - updateInterval(event: number) { + async updateInterval(event: number) { this.selectedInterval = event; - this.planTypeChanged(); + await this.planTypeChanged(); } protected getPlanIntervals() { @@ -460,7 +470,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } } - protected selectPlan(plan: PlanResponse) { + protected async selectPlan(plan: PlanResponse) { if ( this.selectedInterval === PlanInterval.Monthly && plan.productTier == ProductTierType.Families @@ -475,7 +485,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.formGroup.patchValue({ productTier: plan.productTier }); try { - this.refreshSalesTax(); + await this.refreshSalesTax(); } catch { this.estimatedTax = 0; } @@ -489,19 +499,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { get upgradeRequiresPaymentMethod() { const isFreeTier = this.organization?.productTierType === ProductTierType.Free; const shouldHideFree = !this.showFree; - const hasNoPaymentSource = !this.paymentSource; + const hasNoPaymentSource = !this.paymentMethod; return isFreeTier && shouldHideFree && hasNoPaymentSource; } - get selectedSecretsManagerPlan() { - let planResponse: PlanResponse; - if (this.secretsManagerPlans) { - return this.secretsManagerPlans.find((plan) => plan.type === this.selectedPlan.type); - } - return planResponse; - } - get selectedPlanInterval() { if (this.isSubscriptionCanceled) { return this.currentPlan.isAnnual ? "year" : "month"; @@ -591,8 +593,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return 0; } - const result = plan.PasswordManager.seatPrice * Math.abs(this.sub?.seats || 0); - return result; + return plan.PasswordManager.seatPrice * Math.abs(this.sub?.seats || 0); } secretsManagerSeatTotal(plan: PlanResponse, seats: number): number { @@ -746,39 +747,22 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.formGroup.controls.additionalSeats.setValue(1); } - changedCountry() { - this.paymentComponent.showBankAccount = this.taxInformation.country === "US"; - - if ( - !this.paymentComponent.showBankAccount && - this.paymentComponent.selected === PaymentMethodType.BankAccount - ) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - - protected taxInformationChanged(event: TaxInformation): void { - this.taxInformation = event; - this.changedCountry(); - this.refreshSalesTax(); - } - submit = async () => { - if (this.taxComponent !== undefined && !this.taxComponent.validate()) { - this.taxComponent.markAllAsTouched(); + this.formGroup.markAllAsTouched(); + this.billingFormGroup.markAllAsTouched(); + if (this.formGroup.invalid || (this.billingFormGroup.invalid && !this.paymentMethod)) { return; } const doSubmit = async (): Promise => { - const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - let orgId: string = null; + let orgId: string; const sub = this.sub?.subscription; const isCanceled = sub?.status === "canceled"; const isCancelledDowngradedToFreeOrg = sub?.cancelled && this.organization.productTierType === ProductTierType.Free; if (isCanceled || isCancelledDowngradedToFreeOrg) { - await this.restartSubscription(activeUserId); + await this.restartSubscription(); orgId = this.organizationId; } else { orgId = await this.updateOrganization(); @@ -795,9 +779,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { await this.syncService.fullSync(true); if (!this.acceptingSponsorship && !this.isInTrialFlow) { - // 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/" + orgId + "/billing/subscription"]); + await this.router.navigate(["/organizations/" + orgId + "/billing/subscription"]); } if (this.isInTrialFlow) { @@ -818,46 +800,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.dialogRef.close(); }; - private async restartSubscription(activeUserId: UserId) { - const org = await this.organizationApiService.get(this.organizationId); - const organization: OrganizationInformation = { - name: org.name, - 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: filteredPlan.type, - passwordManagerSeats: org.seats, - }; - - if (org.useSecretsManager) { - plan.subscribeToSecretsManager = true; - plan.secretsManagerSeats = org.smSeats; - } - - const { type, token } = await this.paymentComponent.tokenize(); - const paymentMethod: [string, PaymentMethodType] = [token, type]; - - const payment: PaymentInformation = { + private async restartSubscription() { + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + const billingAddress = getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress); + await this.subscriberBillingClient.restartSubscription( + { type: "organization", data: this.organization }, paymentMethod, - billing: this.getBillingInformationFromTaxInfoComponent(), - }; - - await this.organizationBillingService.restartSubscription( - this.organization.id, - { - organization, - plan, - payment, - }, - activeUserId, + billingAddress, ); } @@ -875,25 +824,25 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.formGroup.controls.premiumAccessAddon.value; request.planType = this.selectedPlan.type; if (this.showPayment) { - request.billingAddressCountry = this.taxInformation.country; - request.billingAddressPostalCode = this.taxInformation.postalCode; + request.billingAddressCountry = this.billingFormGroup.controls.billingAddress.value.country; + request.billingAddressPostalCode = + this.billingFormGroup.controls.billingAddress.value.postalCode; } // Secrets Manager this.buildSecretsManagerRequest(request); - if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) { - const tokenizedPaymentSource = await this.paymentComponent.tokenize(); - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, + if (this.upgradeRequiresPaymentMethod || this.showPayment || !this.paymentMethod) { + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + const billingAddress = getBillingAddressFromForm( + this.billingFormGroup.controls.billingAddress, ); - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, - ); + const subscriber: BitwardenSubscriber = { type: "organization", data: this.organization }; + await Promise.all([ + this.subscriberBillingClient.updatePaymentMethod(subscriber, paymentMethod, null), + this.subscriberBillingClient.updateBillingAddress(subscriber, billingAddress), + ]); } // Backfill pub/priv key if necessary @@ -931,18 +880,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return text; } - private getBillingInformationFromTaxInfoComponent(): BillingInformation { - return { - country: this.taxInformation.country, - postalCode: this.taxInformation.postalCode, - taxId: this.taxInformation.taxId, - addressLine1: this.taxInformation.line1, - addressLine2: this.taxInformation.line2, - city: this.taxInformation.city, - state: this.taxInformation.state, - }; - } - private buildSecretsManagerRequest(request: OrganizationUpgradeRequest): void { request.useSecretsManager = this.organization.useSecretsManager; if (!this.organization.useSecretsManager) { @@ -1002,25 +939,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } calculateTotalAppliedDiscount(total: number) { - const discountedTotal = total * (this.discountPercentageFromSub / 100); - return discountedTotal; - } - - get paymentSourceClasses() { - if (this.paymentSource == null) { - return []; - } - switch (this.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - case PaymentMethodType.Check: - return ["bwi-billing"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } + return total * (this.discountPercentageFromSub / 100); } resolvePlanName(productTier: ProductTierType) { @@ -1064,9 +983,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } } - onFocus(index: number) { + async onFocus(index: number) { this.focusedIndex = index; - this.selectPlan(this.selectableProducts[index]); + await this.selectPlan(this.selectableProducts[index]); } isCardDisabled(index: number): boolean { @@ -1078,58 +997,44 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return index; } - private refreshSalesTax(): void { - if ( - this.taxInformation === undefined || - !this.taxInformation.country || - !this.taxInformation.postalCode - ) { + private async refreshSalesTax(): Promise { + if (this.billingFormGroup.controls.billingAddress.invalid && !this.billingAddress) { return; } - const request: PreviewOrganizationInvoiceRequest = { - organizationId: this.organizationId, - passwordManager: { - additionalStorage: 0, - plan: this.selectedPlan?.type, - seats: this.sub.seats, - }, - taxInformation: { - postalCode: this.taxInformation.postalCode, - country: this.taxInformation.country, - taxId: this.taxInformation.taxId, - }, + const getPlanFromLegacyEnum = (planType: PlanType): OrganizationSubscriptionPlan => { + switch (planType) { + case PlanType.FamiliesAnnually: + return { tier: "families", cadence: "annually" }; + case PlanType.TeamsMonthly: + return { tier: "teams", cadence: "monthly" }; + case PlanType.TeamsAnnually: + return { tier: "teams", cadence: "annually" }; + case PlanType.EnterpriseMonthly: + return { tier: "enterprise", cadence: "monthly" }; + case PlanType.EnterpriseAnnually: + return { tier: "enterprise", cadence: "annually" }; + } }; - if (this.organization.useSecretsManager) { - request.secretsManager = { - seats: this.sub.smSeats, - additionalMachineAccounts: - this.sub.smServiceAccounts - this.sub.plan.SecretsManager.baseServiceAccount, - }; - } + const billingAddress = this.billingFormGroup.controls.billingAddress.valid + ? getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress) + : this.billingAddress; - this.taxService - .previewOrganizationInvoice(request) - .then((invoice) => { - this.estimatedTax = invoice.taxAmount; - }) - .catch((error) => { - const translatedMessage = this.i18nService.t(error.message); - this.toastService.showToast({ - title: "", - variant: "error", - message: - !translatedMessage || translatedMessage === "" ? error.message : translatedMessage, - }); - }); + const taxAmounts = await this.taxClient.previewTaxForOrganizationSubscriptionPlanChange( + this.organizationId, + getPlanFromLegacyEnum(this.selectedPlan.type), + billingAddress, + ); + + this.estimatedTax = taxAmounts.tax; } protected canUpdatePaymentInformation(): boolean { return ( this.upgradeRequiresPaymentMethod || this.showPayment || - this.isPaymentSourceEmpty() || + !this.paymentMethod || this.isSubscriptionCanceled ); } @@ -1146,4 +1051,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return this.i18nService.t("upgrade"); } } + + get supportsTaxId() { + return this.formGroup.value.productTier !== ProductTierType.Families; + } + + getCardBrandIcon = () => getCardBrandIcon(this.paymentMethod); } 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 692791db855..5c8df483587 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 @@ -11,7 +11,6 @@ import { WebPlatformUtilsService } from "../../core/web-platform-utils.service"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component"; -import { OrganizationPaymentMethodComponent } from "./payment-method/organization-payment-method.component"; const routes: Routes = [ { @@ -26,17 +25,6 @@ const routes: Routes = [ : OrganizationSubscriptionCloudComponent, data: { titleId: "subscription" }, }, - { - path: "payment-method", - component: OrganizationPaymentMethodComponent, - canActivate: [ - organizationPermissionsGuard((org) => org.canEditPaymentMethods), - organizationIsUnmanaged, - ], - data: { - titleId: "paymentMethod", - }, - }, { path: "payment-details", component: OrganizationPaymentDetailsComponent, 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 707a854de02..90ba04c4fa4 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -17,7 +17,6 @@ import { OrganizationBillingRoutingModule } from "./organization-billing-routing import { OrganizationPlansComponent } from "./organization-plans.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component"; -import { OrganizationPaymentMethodComponent } from "./payment-method/organization-payment-method.component"; import { SecretsManagerAdjustSubscriptionComponent } from "./sm-adjust-subscription.component"; import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component"; import { SubscriptionHiddenComponent } from "./subscription-hidden.component"; @@ -45,7 +44,6 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; SecretsManagerSubscribeStandaloneComponent, SubscriptionHiddenComponent, SubscriptionStatusComponent, - OrganizationPaymentMethodComponent, ], }) export class OrganizationBillingModule {} 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 3b765927c3c..6234fc6e6e3 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -404,17 +404,16 @@

{{ paymentDesc }}

- - - + + } + + > +
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }} 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 820bee950eb..cbeedc454dc 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -11,10 +11,9 @@ import { } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { firstValueFrom, merge, Subject, takeUntil } from "rxjs"; import { debounceTime, map, switchMap } 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 { @@ -32,24 +31,12 @@ import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-conso import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; 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 { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { - PaymentMethodType, - PlanSponsorshipType, - 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 { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; +import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; 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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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"; @@ -59,10 +46,20 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { + OrganizationSubscriptionPlan, + SubscriberBillingClient, + TaxClient, +} from "@bitwarden/web-vault/app/billing/clients"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, + getBillingAddressFromForm, +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { tokenizablePaymentMethodToLegacyEnum } from "@bitwarden/web-vault/app/billing/payment/types"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared"; -import { PaymentComponent } from "../shared/payment/payment.component"; interface OnSuccessArgs { organizationId: string; @@ -78,11 +75,16 @@ const Allowed2020PlansForLegacyProviders = [ @Component({ selector: "app-organization-plans", templateUrl: "organization-plans.component.html", - imports: [BillingSharedModule, OrganizationCreateModule], + imports: [ + BillingSharedModule, + OrganizationCreateModule, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, + ], + providers: [SubscriberBillingClient, TaxClient], }) export class OrganizationPlansComponent implements OnInit, OnDestroy { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent; + @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; @Input() organizationId?: string; @Input() showFree = true; @@ -105,8 +107,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private _productTier = ProductTierType.Free; - protected taxInformation: TaxInformation; - @Input() get plan(): PlanType { return this._plan; @@ -135,10 +135,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder); - selfHostedForm = this.formBuilder.group({ - file: [null, [Validators.required]], - }); - formGroup = this.formBuilder.group({ name: [""], billingEmail: ["", [Validators.email]], @@ -152,6 +148,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { secretsManager: this.secretsManagerSubscription, }); + billingFormGroup = this.formBuilder.group({ + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), + }); + passwordManagerPlans: PlanResponse[]; secretsManagerPlans: PlanResponse[]; organization: Organization; @@ -179,10 +180,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private organizationApiService: OrganizationApiServiceAbstraction, private providerApiService: ProviderApiServiceAbstraction, private toastService: ToastService, - private configService: ConfigService, - private billingApiService: BillingApiServiceAbstraction, - private taxService: TaxServiceAbstraction, private accountService: AccountService, + private subscriberBillingClient: SubscriberBillingClient, + private taxClient: TaxClient, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); } @@ -199,9 +199,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { ); this.billing = await this.organizationApiService.getBilling(this.organizationId); this.sub = await this.organizationApiService.getSubscription(this.organizationId); - this.taxInformation = await this.organizationApiService.getTaxInfo(this.organizationId); - } else if (!this.selfHosted) { - this.taxInformation = await this.apiService.getTaxInfo(); + const billingAddress = await this.subscriberBillingClient.getBillingAddress({ + type: "organization", + data: this.organization, + }); + this.billingFormGroup.controls.billingAddress.patchValue({ + ...billingAddress, + taxId: billingAddress?.taxId?.value, + }); } if (!this.selfHosted) { @@ -268,15 +273,17 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.loading = false; - this.formGroup.valueChanges.pipe(debounceTime(1000), takeUntil(this.destroy$)).subscribe(() => { - this.refreshSalesTax(); - }); - - this.secretsManagerForm.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - this.refreshSalesTax(); - }); + merge( + this.formGroup.valueChanges, + this.billingFormGroup.valueChanges, + this.secretsManagerForm.valueChanges, + ) + .pipe( + debounceTime(1000), + switchMap(async () => await this.refreshSalesTax()), + takeUntil(this.destroy$), + ) + .subscribe(); if (this.enableSecretsManagerByDefault && this.selectedSecretsManagerPlan) { this.secretsManagerSubscription.patchValue({ @@ -587,34 +594,13 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.changedProduct(); } - protected changedCountry(): void { - this.paymentComponent.showBankAccount = this.taxInformation?.country === "US"; - if ( - !this.paymentComponent.showBankAccount && - this.paymentComponent.selected === PaymentMethodType.BankAccount - ) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - - protected onTaxInformationChanged(event: TaxInformation): void { - this.taxInformation = event; - this.changedCountry(); - this.refreshSalesTax(); - } - protected cancel(): void { this.onCanceled.emit(); } - protected setSelectedFile(event: Event): void { - const fileInputEl = event.target; - this.selectedFile = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null; - } - submit = async () => { - if (this.taxComponent && !this.taxComponent.validate()) { - this.taxComponent.markAllAsTouched(); + this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { return; } @@ -688,46 +674,54 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } } - private refreshSalesTax(): void { - if (!this.taxComponent.validate()) { + private async refreshSalesTax(): Promise { + if (this.billingFormGroup.controls.billingAddress.invalid) { return; } - const request: PreviewOrganizationInvoiceRequest = { - organizationId: this.organizationId, - passwordManager: { - additionalStorage: this.formGroup.controls.additionalStorage.value, - plan: this.formGroup.controls.plan.value, - sponsoredPlan: this.planSponsorshipType, - seats: this.formGroup.controls.additionalSeats.value, - }, - taxInformation: { - postalCode: this.taxInformation.postalCode, - country: this.taxInformation.country, - taxId: this.taxInformation.taxId, - }, + const getPlanFromLegacyEnum = (): OrganizationSubscriptionPlan => { + switch (this.formGroup.value.plan) { + case PlanType.FamiliesAnnually: + return { tier: "families", cadence: "annually" }; + case PlanType.TeamsMonthly: + return { tier: "teams", cadence: "monthly" }; + case PlanType.TeamsAnnually: + return { tier: "teams", cadence: "annually" }; + case PlanType.EnterpriseMonthly: + return { tier: "enterprise", cadence: "monthly" }; + case PlanType.EnterpriseAnnually: + return { tier: "enterprise", cadence: "annually" }; + } }; - if (this.secretsManagerForm.controls.enabled.value === true) { - request.secretsManager = { - seats: this.secretsManagerForm.controls.userSeats.value, - additionalMachineAccounts: this.secretsManagerForm.controls.additionalServiceAccounts.value, - }; - } + const billingAddress = getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress); - this.taxService - .previewOrganizationInvoice(request) - .then((invoice) => { - this.estimatedTax = invoice.taxAmount; - this.total = invoice.totalAmount; - }) - .catch((error) => { - this.toastService.showToast({ - title: "", - variant: "error", - message: this.i18nService.t(error.message), - }); - }); + const passwordManagerSeats = + this.formGroup.value.productTier === ProductTierType.Families + ? 1 + : this.formGroup.value.additionalSeats; + + const taxAmounts = await this.taxClient.previewTaxForOrganizationSubscriptionPurchase( + { + ...getPlanFromLegacyEnum(), + passwordManager: { + seats: passwordManagerSeats, + additionalStorage: this.formGroup.value.additionalStorage, + sponsored: false, + }, + secretsManager: this.formGroup.value.secretsManager.enabled + ? { + seats: this.secretsManagerForm.value.userSeats, + additionalServiceAccounts: this.secretsManagerForm.value.additionalServiceAccounts, + standalone: false, + } + : undefined, + }, + billingAddress, + ); + + this.estimatedTax = taxAmounts.tax; + this.total = taxAmounts.total; } private async updateOrganization() { @@ -738,21 +732,24 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.selectedPlan.PasswordManager.hasPremiumAccessOption && this.formGroup.controls.premiumAccessAddon.value; request.planType = this.selectedPlan.type; - request.billingAddressCountry = this.taxInformation?.country; - request.billingAddressPostalCode = this.taxInformation?.postalCode; + request.billingAddressCountry = this.billingFormGroup.value.billingAddress.country; + request.billingAddressPostalCode = this.billingFormGroup.value.billingAddress.postalCode; // Secrets Manager this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod) { - const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); - updatePaymentMethodRequest.paymentSource = await this.paymentComponent.tokenize(); - updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From( - this.taxInformation, - ); - await this.billingApiService.updateOrganizationPaymentMethod( - this.organizationId, - updatePaymentMethodRequest, + if (this.billingFormGroup.invalid) { + return; + } + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + await this.subscriberBillingClient.updatePaymentMethod( + { type: "organization", data: this.organization }, + paymentMethod, + { + country: this.billingFormGroup.value.billingAddress.country, + postalCode: this.billingFormGroup.value.billingAddress.postalCode, + }, ); } @@ -791,23 +788,31 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.selectedPlan.type === PlanType.Free) { request.planType = PlanType.Free; } else { - const { type, token } = await this.paymentComponent.tokenize(); + if (this.billingFormGroup.invalid) { + return; + } - request.paymentToken = token; - request.paymentMethodType = type; + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + + const billingAddress = getBillingAddressFromForm( + this.billingFormGroup.controls.billingAddress, + ); + + request.paymentToken = paymentMethod.token; + request.paymentMethodType = tokenizablePaymentMethodToLegacyEnum(paymentMethod.type); request.additionalSeats = this.formGroup.controls.additionalSeats.value; request.additionalStorageGb = this.formGroup.controls.additionalStorage.value; request.premiumAccessAddon = this.selectedPlan.PasswordManager.hasPremiumAccessOption && this.formGroup.controls.premiumAccessAddon.value; request.planType = this.selectedPlan.type; - request.billingAddressPostalCode = this.taxInformation?.postalCode; - request.billingAddressCountry = this.taxInformation?.country; - request.taxIdNumber = this.taxInformation?.taxId; - request.billingAddressLine1 = this.taxInformation?.line1; - request.billingAddressLine2 = this.taxInformation?.line2; - request.billingAddressCity = this.taxInformation?.city; - request.billingAddressState = this.taxInformation?.state; + request.billingAddressPostalCode = billingAddress.postalCode; + request.billingAddressCountry = billingAddress.country; + request.taxIdNumber = billingAddress.taxId?.value; + request.billingAddressLine1 = billingAddress.line1; + request.billingAddressLine2 = billingAddress.line2; + request.billingAddressCity = billingAddress.city; + request.billingAddressState = billingAddress.state; } // Secrets Manager diff --git a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts index 47742ba0a88..b2bf27e726a 100644 --- a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts +++ b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts @@ -1,15 +1,11 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute } from "@angular/router"; import { BehaviorSubject, - catchError, combineLatest, - EMPTY, filter, firstValueFrom, - from, lastValueFrom, - map, merge, Observable, of, @@ -22,15 +18,13 @@ import { withLatestFrom, } from "rxjs"; -import { - getOrganizationById, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { getById } from "@bitwarden/common/platform/misc"; import { DialogService } from "@bitwarden/components"; import { CommandDefinition, MessageListener } from "@bitwarden/messaging"; import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; @@ -54,13 +48,6 @@ import { TaxIdWarningType } from "@bitwarden/web-vault/app/billing/warnings/type import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -class RedirectError { - constructor( - public path: string[], - public relativeTo: ActivatedRoute, - ) {} -} - type View = { organization: BitwardenSubscriber; paymentMethod: MaskedPaymentMethod | null; @@ -93,24 +80,12 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy { switchMap((userId) => this.organizationService .organizations$(userId) - .pipe(getOrganizationById(this.activatedRoute.snapshot.params.organizationId)), + .pipe(getById(this.activatedRoute.snapshot.params.organizationId)), ), filter((organization): organization is Organization => !!organization), ); private load$: Observable = this.organization$.pipe( - switchMap((organization) => - this.configService - .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) - .pipe( - map((managePaymentDetailsOutsideCheckout) => { - if (!managePaymentDetailsOutsideCheckout) { - throw new RedirectError(["../payment-method"], this.activatedRoute); - } - return organization; - }), - ), - ), mapOrganizationToSubscriber, switchMap(async (organization) => { const getTaxIdWarning = firstValueFrom( @@ -132,14 +107,6 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy { taxIdWarning, }; }), - catchError((error: unknown) => { - if (error instanceof RedirectError) { - return from(this.router.navigate(error.path, { relativeTo: error.relativeTo })).pipe( - switchMap(() => EMPTY), - ); - } - throw error; - }), ); view$: Observable = merge( @@ -159,7 +126,6 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy { private messageListener: MessageListener, private organizationService: OrganizationService, private organizationWarningsService: OrganizationWarningsService, - private router: Router, private subscriberBillingClient: SubscriberBillingClient, ) {} diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html deleted file mode 100644 index ab31147e916..00000000000 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - {{ "loading" | i18n }} - - - - -

- {{ accountCreditHeaderText }} -

-

{{ Math.abs(accountCredit) | currency: "$" }}

-

{{ "creditAppliedDesc" | i18n }}

- -
- - -

{{ "paymentMethod" | i18n }}

-

{{ "noPaymentMethod" | i18n }}

- - - -

- - {{ paymentSource.description }} - - {{ "unverified" | i18n }} -

-
- -

- {{ "paymentChargedWithUnpaidSubscription" | i18n }} -

-
-
-
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 deleted file mode 100644 index 4106ee4f9cd..00000000000 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ /dev/null @@ -1,288 +0,0 @@ -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 { combineLatest, firstValueFrom, from, lastValueFrom, map, switchMap } from "rxjs"; - -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { - OrganizationService, - getOrganizationById, -} 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 { TaxInformation } from "@bitwarden/common/billing/models/domain"; -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; -import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import { BillingNotificationService } from "../../services/billing-notification.service"; -import { - AddCreditDialogResult, - openAddCreditDialog, -} from "../../shared/add-credit-dialog.component"; -import { - AdjustPaymentDialogComponent, - AdjustPaymentDialogResultType, -} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component"; -import { - TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE, - TrialPaymentDialogComponent, -} from "../../shared/trial-payment-dialog/trial-payment-dialog.component"; - -@Component({ - templateUrl: "./organization-payment-method.component.html", - standalone: false, -}) -export class OrganizationPaymentMethodComponent implements OnDestroy { - organizationId!: string; - isUnpaid = false; - accountCredit?: number; - paymentSource?: PaymentSourceResponse; - subscriptionStatus?: string; - organization?: Organization; - organizationSubscriptionResponse?: OrganizationSubscriptionResponse; - - loading = true; - - protected readonly Math = Math; - launchPaymentModalAutomatically = false; - - protected taxInformation?: TaxInformation; - - constructor( - private activatedRoute: ActivatedRoute, - private billingApiService: BillingApiServiceAbstraction, - protected organizationApiService: OrganizationApiServiceAbstraction, - private dialogService: DialogService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private router: Router, - private toastService: ToastService, - private location: Location, - private organizationService: OrganizationService, - private accountService: AccountService, - protected syncService: SyncService, - private billingNotificationService: BillingNotificationService, - private configService: ConfigService, - ) { - combineLatest([ - this.activatedRoute.params, - this.configService.getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout), - ]) - .pipe( - switchMap(([{ organizationId }, managePaymentDetailsOutsideCheckout]) => { - if (this.platformUtilsService.isSelfHost()) { - return from(this.router.navigate(["/settings/subscription"])); - } - - if (managePaymentDetailsOutsideCheckout) { - return from( - this.router.navigate(["../payment-details"], { relativeTo: this.activatedRoute }), - ); - } - - this.organizationId = organizationId; - return from(this.load()); - }), - takeUntilDestroyed(), - ) - .subscribe(); - - const state = this.router.getCurrentNavigation()?.extras?.state; - // In case the above state is undefined or null, we use redundantState - const redundantState: any = location.getState(); - const queryParam = this.activatedRoute.snapshot.queryParamMap.get( - "launchPaymentModalAutomatically", - ); - if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { - this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; - } else if ( - redundantState && - Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") - ) { - this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; - } else { - this.launchPaymentModalAutomatically = queryParam === "true"; - } - } - ngOnDestroy(): void { - this.launchPaymentModalAutomatically = false; - } - - protected addAccountCredit = async (): Promise => { - if (this.subscriptionStatus === "trialing") { - const hasValidBillingAddress = await this.checkBillingAddressForTrialingOrg(); - if (!hasValidBillingAddress) { - return; - } - } - const dialogRef = openAddCreditDialog(this.dialogService, { - data: { - organizationId: this.organizationId, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AddCreditDialogResult.Added) { - await this.load(); - } - }; - - protected load = async (): Promise => { - this.loading = true; - try { - const { accountCredit, paymentSource, subscriptionStatus, taxInformation } = - await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); - this.accountCredit = accountCredit; - this.paymentSource = paymentSource; - this.subscriptionStatus = subscriptionStatus; - this.taxInformation = taxInformation; - this.isUnpaid = this.subscriptionStatus === "unpaid"; - - if (this.organizationId) { - const organizationSubscriptionPromise = this.organizationApiService.getSubscription( - this.organizationId, - ); - - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - if (!userId) { - throw new Error("User ID is not found"); - } - - const organizationPromise = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId)), - ); - - [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ - organizationSubscriptionPromise, - organizationPromise, - ]); - - if (!this.organization) { - throw new Error("Organization is not found"); - } - if (!this.paymentSource) { - throw new Error("Payment source is not found"); - } - } - // If the flag `launchPaymentModalAutomatically` is set to true, - // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. - // This delay ensures that any prior UI/rendering operations complete before triggering the modal. - if (this.launchPaymentModalAutomatically) { - window.setTimeout(async () => { - await this.changePayment(); - this.launchPaymentModalAutomatically = false; - this.location.replaceState(this.location.path(), "", {}); - }, 800); - } - } catch (error) { - this.billingNotificationService.handleError(error); - } finally { - this.loading = false; - } - }; - - protected updatePaymentMethod = async (): Promise => { - const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { - data: { - initialPaymentMethod: this.paymentSource?.type, - organizationId: this.organizationId, - productTier: this.organization?.productTierType, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustPaymentDialogResultType.Submitted) { - await this.load(); - } - }; - - changePayment = async () => { - const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, { - data: { - organizationId: this.organizationId, - subscription: this.organizationSubscriptionResponse!, - productTierType: this.organization!.productTierType, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) { - this.location.replaceState(this.location.path(), "", {}); - if (this.launchPaymentModalAutomatically && !this.organization?.enabled) { - await this.syncService.fullSync(true); - } - this.launchPaymentModalAutomatically = false; - await this.load(); - } - }; - - protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise => { - await this.billingApiService.verifyOrganizationBankAccount(this.organizationId, request); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("verifiedBankAccount"), - }); - }; - - protected get accountCreditHeaderText(): string { - const hasAccountCredit = this.accountCredit && this.accountCredit > 0; - const key = hasAccountCredit ? "accountCredit" : "accountBalance"; - return this.i18nService.t(key); - } - - protected get paymentSourceClasses() { - if (this.paymentSource == null) { - return []; - } - switch (this.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - case PaymentMethodType.Check: - return ["bwi-billing"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } - } - - protected get subscriptionIsUnpaid(): boolean { - return this.subscriptionStatus === "unpaid"; - } - - protected get updatePaymentSourceButtonText(): string { - const key = this.paymentSource == null ? "addPaymentMethod" : "changePaymentMethod"; - return this.i18nService.t(key); - } - - private async checkBillingAddressForTrialingOrg(): Promise { - const hasBillingAddress = this.taxInformation != null; - if (!hasBillingAddress) { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("billingAddressRequiredToAddCredit"), - }); - return false; - } - return true; - } -} diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts index c7a297cc28b..53f72558089 100644 --- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts @@ -15,8 +15,6 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.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 { DialogRef, DialogService } from "@bitwarden/components"; import { OrganizationBillingClient } from "@bitwarden/web-vault/app/billing/clients"; @@ -35,7 +33,6 @@ import { TaxIdWarningTypes } from "@bitwarden/web-vault/app/billing/warnings/typ describe("OrganizationWarningsService", () => { let service: OrganizationWarningsService; - let configService: MockProxy; let dialogService: MockProxy; let i18nService: MockProxy; let organizationApiService: MockProxy; @@ -57,7 +54,6 @@ describe("OrganizationWarningsService", () => { }); beforeEach(() => { - configService = mock(); dialogService = mock(); i18nService = mock(); organizationApiService = mock(); @@ -94,7 +90,6 @@ describe("OrganizationWarningsService", () => { TestBed.configureTestingModule({ providers: [ OrganizationWarningsService, - { provide: ConfigService, useValue: configService }, { provide: DialogService, useValue: dialogService }, { provide: I18nService, useValue: i18nService }, { provide: OrganizationApiServiceAbstraction, useValue: organizationApiService }, @@ -466,7 +461,6 @@ describe("OrganizationWarningsService", () => { } as OrganizationWarningsResponse); dialogService.openSimpleDialog.mockResolvedValue(true); - configService.getFeatureFlag.mockResolvedValue(false); router.navigate.mockResolvedValue(true); service.showInactiveSubscriptionDialog$(organization).subscribe({ @@ -478,11 +472,8 @@ describe("OrganizationWarningsService", () => { acceptButtonText: "Continue", cancelButtonText: "Close", }); - expect(configService.getFeatureFlag).toHaveBeenCalledWith( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); expect(router.navigate).toHaveBeenCalledWith( - ["organizations", "org-id-123", "billing", "payment-method"], + ["organizations", "org-id-123", "billing", "payment-details"], { state: { launchPaymentModalAutomatically: true } }, ); done(); @@ -497,7 +488,6 @@ describe("OrganizationWarningsService", () => { } as OrganizationWarningsResponse); dialogService.openSimpleDialog.mockResolvedValue(true); - configService.getFeatureFlag.mockResolvedValue(true); router.navigate.mockResolvedValue(true); service.showInactiveSubscriptionDialog$(organization).subscribe({ @@ -522,7 +512,6 @@ describe("OrganizationWarningsService", () => { service.showInactiveSubscriptionDialog$(organization).subscribe({ complete: () => { expect(dialogService.openSimpleDialog).toHaveBeenCalled(); - expect(configService.getFeatureFlag).not.toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled(); done(); }, diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts index c6bb1bc231b..46a34def28b 100644 --- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts @@ -16,8 +16,6 @@ import { take } from "rxjs/operators"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -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 } from "@bitwarden/components"; @@ -53,7 +51,6 @@ export class OrganizationWarningsService { taxIdWarningRefreshed$ = this.taxIdWarningRefreshedSubject.asObservable(); constructor( - private configService: ConfigService, private dialogService: DialogService, private i18nService: I18nService, private organizationApiService: OrganizationApiServiceAbstraction, @@ -196,14 +193,8 @@ export class OrganizationWarningsService { cancelButtonText: this.i18nService.t("close"), }); if (confirmed) { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - const route = managePaymentDetailsOutsideCheckout - ? "payment-details" - : "payment-method"; await this.router.navigate( - ["organizations", `${organization.id}`, "billing", route], + ["organizations", `${organization.id}`, "billing", "payment-details"], { state: { launchPaymentModalAutomatically: true }, }, diff --git a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts index c33d805aed7..5f5e3442935 100644 --- a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts +++ b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts @@ -5,7 +5,7 @@ import { DialogService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; import { BitwardenSubscriber } from "../../types"; -import { MaskedPaymentMethod } from "../types"; +import { getCardBrandIcon, MaskedPaymentMethod } from "../types"; import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dialog.component"; @@ -40,9 +40,9 @@ import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dial } @case ("card") {

- @let brandIcon = getBrandIconForCard(); - @if (brandIcon !== null) { - + @let cardBrandIcon = getCardBrandIcon(); + @if (cardBrandIcon !== null) { + } @else { } @@ -74,16 +74,6 @@ export class DisplayPaymentMethodComponent { @Input({ required: true }) paymentMethod!: MaskedPaymentMethod | null; @Output() updated = new EventEmitter(); - protected availableCardIcons: Record = { - amex: "card-amex", - diners: "card-diners-club", - discover: "card-discover", - jcb: "card-jcb", - mastercard: "card-mastercard", - unionpay: "card-unionpay", - visa: "card-visa", - }; - constructor(private dialogService: DialogService) {} changePaymentMethod = async (): Promise => { @@ -100,13 +90,5 @@ export class DisplayPaymentMethodComponent { } }; - protected getBrandIconForCard = (): string | null => { - if (this.paymentMethod?.type !== "card") { - return null; - } - - return this.paymentMethod.brand in this.availableCardIcons - ? this.availableCardIcons[this.paymentMethod.brand] - : null; - }; + protected getCardBrandIcon = () => getCardBrandIcon(this.paymentMethod); } diff --git a/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts b/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts index de2f2f94497..6e356097d32 100644 --- a/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts @@ -11,10 +11,7 @@ import { ToastService, } from "@bitwarden/components"; import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients"; -import { - BillingAddress, - getTaxIdTypeForCountry, -} from "@bitwarden/web-vault/app/billing/payment/types"; +import { BillingAddress } from "@bitwarden/web-vault/app/billing/payment/types"; import { BitwardenSubscriber } from "@bitwarden/web-vault/app/billing/types"; import { TaxIdWarningType, @@ -22,7 +19,10 @@ import { } from "@bitwarden/web-vault/app/billing/warnings/types"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -import { EnterBillingAddressComponent } from "./enter-billing-address.component"; +import { + EnterBillingAddressComponent, + getBillingAddressFromForm, +} from "./enter-billing-address.component"; type DialogParams = { subscriber: BitwardenSubscriber; @@ -104,13 +104,7 @@ export class EditBillingAddressDialogComponent { return; } - const { taxId, ...addressFields } = this.formGroup.getRawValue(); - - const taxIdType = taxId ? getTaxIdTypeForCountry(addressFields.country) : null; - - const billingAddress = taxIdType - ? { ...addressFields, taxId: { code: taxIdType.code, value: taxId! } } - : { ...addressFields, taxId: null }; + const billingAddress = getBillingAddressFromForm(this.formGroup); const result = await this.billingClient.updateBillingAddress( this.dialogParams.subscriber, diff --git a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts index 7659b7ed5ca..3f68c12c897 100644 --- a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts @@ -24,6 +24,17 @@ export interface BillingAddressControls { export type BillingAddressFormGroup = FormGroup>; +export const getBillingAddressFromForm = (formGroup: BillingAddressFormGroup): BillingAddress => + getBillingAddressFromControls(formGroup.getRawValue()); + +export const getBillingAddressFromControls = (controls: BillingAddressControls) => { + const { taxId, ...addressFields } = controls; + const taxIdType = taxId ? getTaxIdTypeForCountry(addressFields.country) : null; + return taxIdType + ? { ...addressFields, taxId: { code: taxIdType.code, value: taxId! } } + : { ...addressFields, taxId: null }; +}; + type Scenario = | { type: "checkout"; @@ -67,54 +78,56 @@ type Scenario = />

-
- - {{ "address1" | i18n }} - - -
-
- - {{ "address2" | i18n }} - - -
-
- - {{ "cityTown" | i18n }} - - -
-
- - {{ "stateProvince" | i18n }} - - -
+ @if (scenario.type === "update") { +
+ + {{ "address1" | i18n }} + + +
+
+ + {{ "address2" | i18n }} + + +
+
+ + {{ "cityTown" | i18n }} + + +
+
+ + {{ "stateProvince" | i18n }} + + +
+ } @if (supportsTaxId$ | async) {
@@ -175,7 +188,7 @@ export class EnterBillingAddressComponent implements OnInit, OnDestroy { this.supportsTaxId$ = this.group.controls.country.valueChanges.pipe( startWith(this.group.value.country ?? this.selectableCountries[0].value), map((country) => { - if (!this.scenario.supportsTaxId) { + if (!this.scenario.supportsTaxId || country === "US") { return false; } diff --git a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts index 93c45b873fe..4af5226e7ee 100644 --- a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts @@ -8,7 +8,6 @@ import { PopoverModule, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; -import { PaymentLabelComponent } from "../../shared/payment/payment-label.component"; import { isTokenizablePaymentMethod, selectableCountries, @@ -16,6 +15,8 @@ import { TokenizedPaymentMethod, } from "../types"; +import { PaymentLabelComponent } from "./payment-label.component"; + type PaymentMethodOption = TokenizablePaymentMethod | "accountCredit"; type PaymentMethodFormGroup = FormGroup<{ @@ -102,7 +103,7 @@ type PaymentMethodFormGroup = FormGroup<{ - - - - -
- - - - - - - - - - - - - - - -
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 deleted file mode 100644 index cdf72168acf..00000000000 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ /dev/null @@ -1,191 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -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"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; - -export interface AddCreditDialogData { - organizationId: string; -} - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum AddCreditDialogResult { - Added = "added", - Cancelled = "cancelled", -} - -export type PayPalConfig = { - businessId?: string; - buttonAction?: string; -}; - -@Component({ - templateUrl: "add-credit-dialog.component.html", - standalone: false, -}) -export class AddCreditDialogComponent implements OnInit { - @ViewChild("ppButtonForm", { read: ElementRef, static: true }) ppButtonFormRef: ElementRef; - - paymentMethodType = PaymentMethodType; - ppButtonFormAction: string; - ppButtonBusinessId: string; - ppButtonCustomField: string; - ppLoading = false; - subject: string; - returnUrl: string; - organizationId: string; - - private userId: string; - private name: string; - private email: string; - private region: string; - - protected DialogResult = AddCreditDialogResult; - protected formGroup = new FormGroup({ - method: new FormControl(PaymentMethodType.PayPal), - creditAmount: new FormControl(null, [Validators.required]), - }); - - constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AddCreditDialogData, - private accountService: AccountService, - private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, - private organizationService: OrganizationService, - private logService: LogService, - private configService: ConfigService, - ) { - this.organizationId = data.organizationId; - const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig; - this.ppButtonFormAction = payPalConfig.buttonAction; - this.ppButtonBusinessId = payPalConfig.businessId; - } - - async ngOnInit() { - if (this.organizationId != null) { - if (this.creditAmount == null) { - this.creditAmount = "0.00"; - } - this.ppButtonCustomField = "organization_id:" + 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; - } - } else { - if (this.creditAmount == null) { - this.creditAmount = "0.00"; - } - const [userId, email] = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), - ); - this.userId = userId; - this.subject = email; - this.email = this.subject; - this.ppButtonCustomField = "user_id:" + this.userId; - } - this.region = await firstValueFrom(this.configService.cloudRegion$); - this.ppButtonCustomField += ",account_credit:1"; - this.ppButtonCustomField += `,region:${this.region}`; - this.returnUrl = window.location.href; - } - - get creditAmount() { - return this.formGroup.value.creditAmount; - } - set creditAmount(value: string) { - this.formGroup.get("creditAmount").setValue(value); - } - - get method() { - return this.formGroup.value.method; - } - - submit = async () => { - if (this.creditAmount == null || this.creditAmount === "") { - return; - } - - if (this.method === PaymentMethodType.PayPal) { - this.ppButtonFormRef.nativeElement.submit(); - this.ppLoading = true; - return; - } - if (this.method === PaymentMethodType.BitPay) { - const req = new BitPayInvoiceRequest(); - req.email = this.email; - req.name = this.name; - req.credit = true; - req.amount = this.creditAmountNumber; - req.organizationId = this.organizationId; - req.userId = this.userId; - req.returnUrl = this.returnUrl; - const bitPayUrl: string = await this.apiService.postBitPayInvoice(req); - this.platformUtilsService.launchUri(bitPayUrl); - return; - } - this.dialogRef.close(AddCreditDialogResult.Added); - }; - - formatAmount() { - try { - if (this.creditAmount != null && this.creditAmount !== "") { - const floatAmount = Math.abs(parseFloat(this.creditAmount)); - if (floatAmount > 0) { - this.creditAmount = parseFloat((Math.round(floatAmount * 100) / 100).toString()) - .toFixed(2) - .toString(); - return; - } - } - } catch (e) { - this.logService.error(e); - } - this.creditAmount = ""; - } - - get creditAmountNumber(): number { - if (this.creditAmount != null && this.creditAmount !== "") { - try { - return parseFloat(this.creditAmount); - } catch (e) { - this.logService.error(e); - } - } - return null; - } -} - -/** - * Strongly typed helper to open a AddCreditDialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ -export function openAddCreditDialog( - dialogService: DialogService, - config: DialogConfig, -) { - return dialogService.open(AddCreditDialogComponent, config); -} 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 deleted file mode 100644 index 9c70908af8e..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - 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 deleted file mode 100644 index 9944085488f..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ /dev/null @@ -1,225 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -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 { - DIALOG_DATA, - DialogConfig, - DialogRef, - DialogService, - ToastService, -} from "@bitwarden/components"; - -import { PaymentComponent } from "../payment/payment.component"; - -export interface AdjustPaymentDialogParams { - initialPaymentMethod?: PaymentMethodType | null; - organizationId?: string; - productTier?: ProductTierType; - providerId?: string; -} - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum AdjustPaymentDialogResultType { - Closed = "closed", - Submitted = "submitted", -} - -@Component({ - templateUrl: "./adjust-payment-dialog.component.html", - standalone: false, -}) -export class AdjustPaymentDialogComponent implements OnInit { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(forwardRef(() => ManageTaxInformationComponent)) - taxInfoComponent: ManageTaxInformationComponent; - - protected readonly PaymentMethodType = PaymentMethodType; - protected readonly ResultType = AdjustPaymentDialogResultType; - - protected dialogHeader: string; - protected initialPaymentMethod: PaymentMethodType; - protected organizationId?: string; - protected productTier?: ProductTierType; - protected providerId?: string; - - protected loading = true; - - protected taxInformation: TaxInformation; - - constructor( - private apiService: ApiService, - private billingApiService: BillingApiServiceAbstraction, - private organizationApiService: OrganizationApiServiceAbstraction, - @Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogParams, - 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; - this.providerId = this.dialogParams.providerId; - } - - ngOnInit(): void { - if (this.organizationId) { - this.organizationApiService - .getTaxInfo(this.organizationId) - .then((response: TaxInfoResponse) => { - this.taxInformation = TaxInformation.from(response); - this.toggleBankAccount(); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }) - .finally(() => { - this.loading = false; - }); - } else if (this.providerId) { - this.billingApiService - .getProviderTaxInformation(this.providerId) - .then((response) => { - this.taxInformation = TaxInformation.from(response); - this.toggleBankAccount(); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }) - .finally(() => { - this.loading = false; - }); - } else { - this.apiService - .getTaxInfo() - .then((response: TaxInfoResponse) => { - this.taxInformation = TaxInformation.from(response); - }) - .catch(() => { - this.taxInformation = new TaxInformation(); - }) - .finally(() => { - this.loading = false; - }); - } - } - - taxInformationChanged(event: TaxInformation) { - this.taxInformation = event; - this.toggleBankAccount(); - } - - toggleBankAccount = () => { - if (this.taxInformation.country === "US") { - this.paymentComponent.showBankAccount = !!this.organizationId || !!this.providerId; - } else { - this.paymentComponent.showBankAccount = false; - if (this.paymentComponent.selected === PaymentMethodType.BankAccount) { - this.paymentComponent.select(PaymentMethodType.Card); - } - } - }; - - submit = async (): Promise => { - if (!this.taxInfoComponent.validate()) { - this.taxInfoComponent.markAllAsTouched(); - return; - } - - try { - if (this.organizationId) { - await this.updateOrganizationPaymentMethod(); - } else if (this.providerId) { - await this.updateProviderPaymentMethod(); - } else { - await this.updatePremiumUserPaymentMethod(); - } - - 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); - }; - - 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); - }; - - private updateProviderPaymentMethod = async () => { - const paymentSource = await this.paymentComponent.tokenize(); - - const request = new UpdatePaymentMethodRequest(); - request.paymentSource = paymentSource; - request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); - - await this.billingApiService.updateProviderPaymentMethod(this.providerId, request); - }; - - protected get showTaxIdField(): boolean { - if (this.organizationId) { - switch (this.productTier) { - case ProductTierType.Free: - case ProductTierType.Families: - return false; - default: - return true; - } - } else { - return !!this.providerId; - } - } - - static open = ( - dialogService: DialogService, - dialogConfig: DialogConfig, - ) => - dialogService.open(AdjustPaymentDialogComponent, 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 7322f047551..fb593b39328 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -1,46 +1,40 @@ import { NgModule } from "@angular/core"; import { BannerModule } from "@bitwarden/components"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, +} from "@bitwarden/web-vault/app/billing/payment/components"; import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; -import { AddCreditDialogComponent } from "./add-credit-dialog.component"; -import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component"; import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; -import { PaymentComponent } from "./payment/payment.component"; -import { PaymentMethodComponent } from "./payment-method.component"; import { PlanCardComponent } from "./plan-card/plan-card.component"; import { PricingSummaryComponent } from "./pricing-summary/pricing-summary.component"; import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component"; import { OrganizationSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/organization-self-hosting-license-uploader.component"; import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component"; -import { TaxInfoComponent } from "./tax-info.component"; import { TrialPaymentDialogComponent } from "./trial-payment-dialog/trial-payment-dialog.component"; import { UpdateLicenseDialogComponent } from "./update-license-dialog.component"; import { UpdateLicenseComponent } from "./update-license.component"; -import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-account.component"; @NgModule({ imports: [ SharedModule, - TaxInfoComponent, HeaderModule, BannerModule, - PaymentComponent, - VerifyBankAccountComponent, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, ], declarations: [ - AddCreditDialogComponent, BillingHistoryComponent, - PaymentMethodComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, - AdjustPaymentDialogComponent, AdjustStorageDialogComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, @@ -50,14 +44,11 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac ], exports: [ SharedModule, - TaxInfoComponent, BillingHistoryComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, - VerifyBankAccountComponent, - PaymentComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, ], diff --git a/apps/web/src/app/billing/shared/index.ts b/apps/web/src/app/billing/shared/index.ts index 54ab5bc0a2a..466d1d3e586 100644 --- a/apps/web/src/app/billing/shared/index.ts +++ b/apps/web/src/app/billing/shared/index.ts @@ -1,4 +1,2 @@ export * from "./billing-shared.module"; -export * from "./payment-method.component"; export * from "./sm-subscribe.component"; -export * from "./tax-info.component"; diff --git a/apps/web/src/app/billing/shared/payment-method.component.html b/apps/web/src/app/billing/shared/payment-method.component.html deleted file mode 100644 index 81ed7e5a631..00000000000 --- a/apps/web/src/app/billing/shared/payment-method.component.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - -

{{ "paymentMethod" | i18n }}

- - - - {{ "loading" | i18n }} - - - -

- {{ (isCreditBalance ? "accountCredit" : "accountBalance") | i18n }} -

-

{{ creditOrBalance | currency: "$" }}

-

{{ "creditAppliedDesc" | i18n }}

- -
- -

{{ "paymentMethod" | i18n }}

-

{{ "noPaymentMethod" | i18n }}

- - -

- {{ "verifyBankAccountDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }} -

-
- - {{ "amountX" | i18n: "1" }} - - $0. - - - {{ "amountX" | i18n: "2" }} - - $0. - - -
-
-

- - {{ paymentSource.description }} -

-
- -

- {{ "paymentChargedWithUnpaidSubscription" | i18n }} -

-
-
-
diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts deleted file mode 100644 index b6431843b83..00000000000 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ /dev/null @@ -1,261 +0,0 @@ -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 { 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, - getOrganizationById, -} 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"; -import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank.request"; -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/platform/sync"; -import { DialogService, ToastService } from "@bitwarden/components"; - -import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; -import { - AdjustPaymentDialogComponent, - AdjustPaymentDialogResultType, -} from "./adjust-payment-dialog/adjust-payment-dialog.component"; - -@Component({ - templateUrl: "payment-method.component.html", - standalone: false, -}) -export class PaymentMethodComponent implements OnInit, OnDestroy { - loading = false; - firstLoaded = false; - billing?: BillingPaymentResponse; - org?: OrganizationSubscriptionResponse; - sub?: SubscriptionResponse; - paymentMethodType = PaymentMethodType; - organizationId?: string; - isUnpaid = false; - organization?: Organization; - - verifyBankForm = this.formBuilder.group({ - amount1: new FormControl(0, [ - Validators.required, - Validators.max(99), - Validators.min(0), - ]), - amount2: new FormControl(0, [ - Validators.required, - Validators.max(99), - Validators.min(0), - ]), - }); - - launchPaymentModalAutomatically = false; - constructor( - protected apiService: ApiService, - protected organizationApiService: OrganizationApiServiceAbstraction, - protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, - private router: Router, - private location: Location, - private route: ActivatedRoute, - private formBuilder: FormBuilder, - private dialogService: DialogService, - private toastService: ToastService, - private organizationService: OrganizationService, - private accountService: AccountService, - protected syncService: SyncService, - private configService: ConfigService, - ) { - const state = this.router.getCurrentNavigation()?.extras?.state; - // In case the above state is undefined or null, we use redundantState - const redundantState: any = location.getState(); - if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { - this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; - } else if ( - redundantState && - Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") - ) { - this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; - } else { - this.launchPaymentModalAutomatically = false; - } - } - - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.params.subscribe(async (params) => { - if (params.organizationId) { - this.organizationId = params.organizationId; - } else if (this.platformUtilsService.isSelfHost()) { - // 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"]); - return; - } - - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - - if (managePaymentDetailsOutsideCheckout) { - await this.router.navigate(["../payment-details"], { relativeTo: this.route }); - } - - await this.load(); - this.firstLoaded = true; - }); - } - - load = async () => { - if (this.loading) { - return; - } - this.loading = true; - if (this.forOrganization) { - const billingPromise = this.organizationApiService.getBilling(this.organizationId!); - const organizationSubscriptionPromise = this.organizationApiService.getSubscription( - this.organizationId!, - ); - - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - if (!userId) { - throw new Error("User ID is not found"); - } - - const organizationPromise = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId!)), - ); - - [this.billing, this.org, this.organization] = await Promise.all([ - billingPromise, - organizationSubscriptionPromise, - organizationPromise, - ]); - } else { - const billingPromise = this.apiService.getUserBillingPayment(); - const subPromise = this.apiService.getUserSubscription(); - - [this.billing, this.sub] = await Promise.all([billingPromise, subPromise]); - } - // TODO: Eslint upgrade. Please resolve this since the ?? does nothing - // eslint-disable-next-line no-constant-binary-expression - this.isUnpaid = this.subscription?.status === "unpaid" ?? false; - this.loading = false; - // If the flag `launchPaymentModalAutomatically` is set to true, - // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. - // This delay ensures that any prior UI/rendering operations complete before triggering the modal. - if (this.launchPaymentModalAutomatically) { - window.setTimeout(async () => { - await this.changePayment(); - this.launchPaymentModalAutomatically = false; - this.location.replaceState(this.location.path(), "", {}); - }, 800); - } - }; - - addCredit = async () => { - if (this.forOrganization) { - const dialogRef = openAddCreditDialog(this.dialogService, { - data: { - organizationId: this.organizationId!, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AddCreditDialogResult.Added) { - await this.load(); - } - } - }; - - changePayment = async () => { - const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { - data: { - organizationId: this.organizationId, - initialPaymentMethod: this.paymentSource !== null ? this.paymentSource.type : null, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustPaymentDialogResultType.Submitted) { - this.location.replaceState(this.location.path(), "", {}); - if (this.launchPaymentModalAutomatically && !this.organization?.enabled) { - await this.syncService.fullSync(true); - } - this.launchPaymentModalAutomatically = false; - await this.load(); - } - }; - - verifyBank = async () => { - if (this.loading || !this.forOrganization) { - return; - } - - const request = new VerifyBankRequest(); - request.amount1 = this.verifyBankForm.value.amount1!; - request.amount2 = this.verifyBankForm.value.amount2!; - await this.organizationApiService.verifyBank(this.organizationId!, request); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("verifiedBankAccount"), - }); - await this.load(); - }; - - get isCreditBalance() { - return this.billing == null || this.billing.balance <= 0; - } - - get creditOrBalance() { - return Math.abs(this.billing != null ? this.billing.balance : 0); - } - - get paymentSource() { - return this.billing != null ? this.billing.paymentSource : null; - } - - get forOrganization() { - return this.organizationId != null; - } - - get paymentSourceClasses() { - if (this.paymentSource == null) { - return []; - } - switch (this.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - case PaymentMethodType.Check: - return ["bwi-billing"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } - } - - get subscription() { - return this.sub?.subscription ?? this.org?.subscription ?? null; - } - - ngOnDestroy(): void { - this.launchPaymentModalAutomatically = false; - } -} diff --git a/apps/web/src/app/billing/shared/payment/payment-label.component.html b/apps/web/src/app/billing/shared/payment/payment-label.component.html deleted file mode 100644 index a931b0524e3..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment-label.component.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - -
- - - ({{ "required" | i18n }}) - -
diff --git a/apps/web/src/app/billing/shared/payment/payment.component.html b/apps/web/src/app/billing/shared/payment/payment.component.html deleted file mode 100644 index d1356c20854..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment.component.html +++ /dev/null @@ -1,149 +0,0 @@ -
-
- - - - - {{ "creditCard" | i18n }} - - - - - - {{ "bankAccount" | i18n }} - - - - - - {{ "payPal" | i18n }} - - - - - - {{ "accountCredit" | i18n }} - - - -
- - -
-
- - {{ "number" | i18n }} - -
-
-
- Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay -
-
- - {{ "expiration" | i18n }} - -
-
-
- - {{ "securityCodeSlashCVV" | i18n }} - - - - -
-
-
-
- - - - {{ "requiredToVerifyBankAccountWithStripe" | i18n }} - -
- - {{ "routingNumber" | 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 deleted file mode 100644 index 08476e9952f..00000000000 --- a/apps/web/src/app/billing/shared/payment/payment.component.ts +++ /dev/null @@ -1,215 +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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { SharedModule } from "../../../shared"; -import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; - -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", - imports: [BillingServicesModule, SharedModule, PaymentLabelComponent], -}) -export class PaymentComponent 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; - - @Input() private bankAccountWarningOverride?: string; - - @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 i18nService: I18nService, - 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); - }; - - validate = () => { - if (!this.usingBankAccount) { - return true; - } - - this.formGroup.controls.bankInformation.markAllAsTouched(); - return this.formGroup.controls.bankInformation.valid; - }; - - /** - * 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/tax-info.component.html b/apps/web/src/app/billing/shared/tax-info.component.html deleted file mode 100644 index ca2ae046f6e..00000000000 --- a/apps/web/src/app/billing/shared/tax-info.component.html +++ /dev/null @@ -1,83 +0,0 @@ -
-
-
- - {{ "country" | i18n }} - - - - -
-
- - {{ "zipPostalCode" | i18n }} - - -
-
- - {{ "address1" | i18n }} - - -
-
- - {{ "address2" | i18n }} - - -
-
- - {{ "cityTown" | i18n }} - - -
-
- - {{ "stateProvince" | i18n }} - - -
-
- - {{ "taxIdNumber" | i18n }} - - -
-
-
diff --git a/apps/web/src/app/billing/shared/tax-info.component.ts b/apps/web/src/app/billing/shared/tax-info.component.ts deleted file mode 100644 index 35c4a3fcc4e..00000000000 --- a/apps/web/src/app/billing/shared/tax-info.component.ts +++ /dev/null @@ -1,199 +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 { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; -import { debounceTime } from "rxjs/operators"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { CountryListItem } from "@bitwarden/common/billing/models/domain"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; - -import { SharedModule } from "../../shared"; - -/** - * @deprecated Use `ManageTaxInformationComponent` instead. - */ -@Component({ - selector: "app-tax-info", - templateUrl: "tax-info.component.html", - imports: [SharedModule], -}) -export class TaxInfoComponent implements OnInit, OnDestroy { - private destroy$ = new Subject(); - - @Input() trialFlow = false; - @Output() countryChanged = new EventEmitter(); - @Output() taxInformationChanged: EventEmitter = new EventEmitter(); - - taxFormGroup = new FormGroup({ - country: new FormControl(null, [Validators.required]), - postalCode: new FormControl(null, [Validators.required]), - taxId: new FormControl(null), - line1: new FormControl(null), - line2: new FormControl(null), - city: new FormControl(null), - state: new FormControl(null), - }); - - protected isTaxSupported: boolean; - - loading = true; - organizationId: string; - providerId: string; - countryList: CountryListItem[] = this.taxService.getCountries(); - - constructor( - private apiService: ApiService, - private route: ActivatedRoute, - private logService: LogService, - private organizationApiService: OrganizationApiServiceAbstraction, - private taxService: TaxServiceAbstraction, - ) {} - - get country(): string { - return this.taxFormGroup.controls.country.value; - } - - get postalCode(): string { - return this.taxFormGroup.controls.postalCode.value; - } - - get taxId(): string { - return this.taxFormGroup.controls.taxId.value; - } - - get line1(): string { - return this.taxFormGroup.controls.line1.value; - } - - get line2(): string { - return this.taxFormGroup.controls.line2.value; - } - - get city(): string { - return this.taxFormGroup.controls.city.value; - } - - get state(): string { - return this.taxFormGroup.controls.state.value; - } - - get showTaxIdField(): boolean { - return !!this.organizationId; - } - - async ngOnInit() { - // Provider setup - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.route.queryParams.subscribe((params) => { - this.providerId = params.providerId; - }); - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent?.parent?.params.subscribe(async (params) => { - this.organizationId = params.organizationId; - if (this.organizationId) { - try { - const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); - if (taxInfo) { - this.taxFormGroup.controls.taxId.setValue(taxInfo.taxId); - this.taxFormGroup.controls.state.setValue(taxInfo.state); - this.taxFormGroup.controls.line1.setValue(taxInfo.line1); - this.taxFormGroup.controls.line2.setValue(taxInfo.line2); - this.taxFormGroup.controls.city.setValue(taxInfo.city); - this.taxFormGroup.controls.postalCode.setValue(taxInfo.postalCode); - this.taxFormGroup.controls.country.setValue(taxInfo.country); - } - } catch (e) { - this.logService.error(e); - } - } else { - try { - const taxInfo = await this.apiService.getTaxInfo(); - if (taxInfo) { - this.taxFormGroup.controls.postalCode.setValue(taxInfo.postalCode); - this.taxFormGroup.controls.country.setValue(taxInfo.country); - } - } catch (e) { - this.logService.error(e); - } - } - - this.isTaxSupported = await this.taxService.isCountrySupported( - this.taxFormGroup.controls.country.value, - ); - - this.countryChanged.emit(); - }); - - this.taxFormGroup.controls.country.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe((value) => { - this.taxService - .isCountrySupported(this.taxFormGroup.controls.country.value) - .then((isSupported) => { - this.isTaxSupported = isSupported; - }) - .catch(() => { - this.isTaxSupported = false; - }) - .finally(() => { - if (!this.isTaxSupported) { - this.taxFormGroup.controls.taxId.setValue(null); - this.taxFormGroup.controls.line1.setValue(null); - this.taxFormGroup.controls.line2.setValue(null); - this.taxFormGroup.controls.city.setValue(null); - this.taxFormGroup.controls.state.setValue(null); - } - - this.countryChanged.emit(); - }); - this.taxInformationChanged.emit(); - }); - - this.taxFormGroup.controls.postalCode.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - this.taxInformationChanged.emit(); - }); - - this.taxFormGroup.controls.taxId.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - this.taxInformationChanged.emit(); - }); - - this.loading = false; - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - submitTaxInfo(): Promise { - this.taxFormGroup.updateValueAndValidity(); - this.taxFormGroup.markAllAsTouched(); - - const request = new ExpandedTaxInfoUpdateRequest(); - request.country = this.country; - request.postalCode = this.postalCode; - request.taxId = this.taxId; - request.line1 = this.line1; - request.line2 = this.line2; - request.city = this.city; - request.state = this.state; - - return this.organizationId - ? this.organizationApiService.updateTaxInfo( - this.organizationId, - request as ExpandedTaxInfoUpdateRequest, - ) - : this.apiService.putTaxInfo(request); - } -} diff --git a/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html index dbd2899c9e0..1b416eae1bc 100644 --- a/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html +++ b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html @@ -86,17 +86,13 @@

{{ "paymentMethod" | i18n }}

- - + + + + (); protected initialPaymentMethod: PaymentMethodType; - protected taxInformation!: TaxInformation; protected readonly ResultType = TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE; pricingSummaryData!: PricingSummaryData; + formGroup = new FormGroup({ + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), + }); + + private destroy$ = new Subject(); + constructor( @Inject(DIALOG_DATA) private dialogParams: TrialPaymentDialogParams, private dialogRef: DialogRef, @@ -93,8 +110,9 @@ export class TrialPaymentDialogComponent implements OnInit { private pricingSummaryService: PricingSummaryService, private apiService: ApiService, private toastService: ToastService, - private billingApiService: BillingApiServiceAbstraction, private organizationBillingApiServiceAbstraction: OrganizationBillingApiServiceAbstraction, + private subscriberBillingClient: SubscriberBillingClient, + private taxClient: TaxClient, ) { this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; } @@ -134,19 +152,48 @@ export class TrialPaymentDialogComponent implements OnInit { : PlanInterval.Monthly; } - const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); - this.taxInformation = TaxInformation.from(taxInfo); + const billingAddress = await this.subscriberBillingClient.getBillingAddress({ + type: "organization", + data: this.organization, + }); - this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( - this.currentPlan, - this.sub, - this.organization, - this.selectedInterval, - this.taxInformation, - this.isSecretsManagerTrial(), - ); + if (billingAddress) { + const { taxId, ...location } = billingAddress; + + this.formGroup.controls.billingAddress.patchValue({ + ...location, + taxId: taxId ? taxId.value : null, + }); + } + + await this.refreshPricingSummary(); this.plans = await this.apiService.getPlans(); + + combineLatest([ + this.formGroup.controls.billingAddress.controls.country.valueChanges.pipe( + startWith(this.formGroup.controls.billingAddress.controls.country.value), + ), + this.formGroup.controls.billingAddress.controls.postalCode.valueChanges.pipe( + startWith(this.formGroup.controls.billingAddress.controls.postalCode.value), + ), + this.formGroup.controls.billingAddress.controls.taxId.valueChanges.pipe( + startWith(this.formGroup.controls.billingAddress.controls.taxId.value), + ), + ]) + .pipe( + debounceTime(500), + switchMap(() => { + return this.refreshPricingSummary(); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } static open = ( @@ -175,14 +222,7 @@ export class TrialPaymentDialogComponent implements OnInit { await this.selectPlan(); - this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( - this.currentPlan, - this.sub, - this.organization, - this.selectedInterval, - this.taxInformation, - this.isSecretsManagerTrial(), - ); + await this.refreshPricingSummary(); } protected async selectPlan() { @@ -202,7 +242,7 @@ export class TrialPaymentDialogComponent implements OnInit { this.currentPlan = filteredPlans[0]; } try { - await this.refreshSalesTax(); + await this.refreshPricingSummary(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const translatedMessage = this.i18nService.t(errorMessage); @@ -214,72 +254,57 @@ export class TrialPaymentDialogComponent implements OnInit { } } - protected get showTaxIdField(): boolean { - switch (this.currentPlan.productTier) { - case ProductTierType.Free: - case ProductTierType.Families: - return false; - default: - return true; - } - } - - private async refreshSalesTax(): Promise { - if ( - this.taxInformation === undefined || - !this.taxInformation.country || - !this.taxInformation.postalCode - ) { - return; - } - - const request: PreviewOrganizationInvoiceRequest = { - organizationId: this.organizationId, - passwordManager: { - additionalStorage: 0, - plan: this.currentPlan?.type, - seats: this.sub.seats, - }, - taxInformation: { - postalCode: this.taxInformation.postalCode, - country: this.taxInformation.country, - taxId: this.taxInformation.taxId, - }, - }; - - if (this.organization.useSecretsManager) { - request.secretsManager = { - seats: this.sub.smSeats ?? 0, - additionalMachineAccounts: - (this.sub.smServiceAccounts ?? 0) - - (this.sub.plan.SecretsManager?.baseServiceAccount ?? 0), - }; - } - + private refreshPricingSummary = async () => { + const estimatedTax = await this.getEstimatedTax(); this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( this.currentPlan, this.sub, this.organization, this.selectedInterval, - this.taxInformation, this.isSecretsManagerTrial(), + estimatedTax, ); - } + }; - async taxInformationChanged(event: TaxInformation) { - this.taxInformation = event; - this.toggleBankAccount(); - await this.refreshSalesTax(); - } + private getEstimatedTax = async () => { + if (this.formGroup.controls.billingAddress.invalid) { + return 0; + } - toggleBankAccount = () => { - this.paymentComponent.showBankAccount = this.taxInformation.country === "US"; + const cadence = + this.currentPlan.productTier !== ProductTierType.Families + ? this.currentPlan.isAnnual + ? "annually" + : "monthly" + : null; - if ( - !this.paymentComponent.showBankAccount && - this.paymentComponent.selected === PaymentMethodType.BankAccount - ) { - this.paymentComponent.select(PaymentMethodType.Card); + const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); + + const getTierFromLegacyEnum = (organization: Organization) => { + switch (organization.productTierType) { + case ProductTierType.Families: + return "families"; + case ProductTierType.Teams: + return "teams"; + case ProductTierType.Enterprise: + return "enterprise"; + } + }; + + const tier = getTierFromLegacyEnum(this.organization); + + if (tier && cadence) { + const costs = await this.taxClient.previewTaxForOrganizationSubscriptionPlanChange( + this.organization.id, + { + tier, + cadence, + }, + billingAddress, + ); + return costs.tax; + } else { + return 0; } }; @@ -292,15 +317,24 @@ export class TrialPaymentDialogComponent implements OnInit { } async onSubscribe(): Promise { - if (!this.taxComponent.validate()) { - this.taxComponent.markAllAsTouched(); + this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { + return; } + try { - await this.updateOrganizationPaymentMethod( - this.organizationId, - this.paymentComponent, - this.taxInformation, - ); + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + if (!paymentMethod) { + return; + } + + const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); + + const subscriber: BitwardenSubscriber = { type: "organization", data: this.organization }; + await Promise.all([ + this.subscriberBillingClient.updatePaymentMethod(subscriber, paymentMethod, null), + this.subscriberBillingClient.updateBillingAddress(subscriber, billingAddress), + ]); if (this.currentPlan.type !== this.sub.planType) { const changePlanRequest = new ChangePlanFrequencyRequest(); @@ -332,20 +366,6 @@ export class TrialPaymentDialogComponent implements OnInit { } } - private async updateOrganizationPaymentMethod( - organizationId: string, - paymentComponent: PaymentComponent, - taxInformation: TaxInformation, - ): Promise { - const paymentSource = await paymentComponent.tokenize(); - - const request = new UpdatePaymentMethodRequest(); - request.paymentSource = paymentSource; - request.taxInformation = ExpandedTaxInfoUpdateRequest.From(taxInformation); - - await this.billingApiService.updateOrganizationPaymentMethod(organizationId, request); - } - resolvePlanName(productTier: ProductTierType): string { switch (productTier) { case ProductTierType.Enterprise: @@ -362,4 +382,11 @@ export class TrialPaymentDialogComponent implements OnInit { return this.i18nService.t("planNameFree"); } } + + get supportsTaxId() { + if (!this.organization) { + return false; + } + return this.organization.productTierType !== ProductTierType.Families; + } } diff --git a/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.html b/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.html deleted file mode 100644 index 1367e6e3082..00000000000 --- a/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.html +++ /dev/null @@ -1,12 +0,0 @@ - -

{{ "verifyBankAccountWithStatementDescriptorInstructions" | i18n }}

-
- - {{ "descriptorCode" | i18n }} - - - -
-
diff --git a/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.ts b/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.ts deleted file mode 100644 index b7cdfbe60a2..00000000000 --- a/apps/web/src/app/billing/shared/verify-bank-account/verify-bank-account.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { FormBuilder, FormControl, Validators } from "@angular/forms"; - -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; - -import { SharedModule } from "../../../shared"; - -@Component({ - selector: "app-verify-bank-account", - templateUrl: "./verify-bank-account.component.html", - imports: [SharedModule], -}) -export class VerifyBankAccountComponent { - @Input() onSubmit?: (request: VerifyBankAccountRequest) => Promise; - @Output() submitted = new EventEmitter(); - - protected formGroup = this.formBuilder.group({ - descriptorCode: new FormControl(null, [ - Validators.required, - Validators.minLength(6), - Validators.maxLength(6), - ]), - }); - - constructor(private formBuilder: FormBuilder) {} - - submit = async () => { - const request = new VerifyBankAccountRequest(this.formGroup.value.descriptorCode); - await this.onSubmit?.(request); - this.submitted.emit(); - }; -} diff --git a/apps/web/src/app/billing/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 index c1a33a4c8df..7377fc45484 100644 --- a/apps/web/src/app/billing/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 @@ -54,17 +54,7 @@ > diff --git a/apps/web/src/app/billing/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 index 0b1ddda0c12..baccabdc763 100644 --- a/apps/web/src/app/billing/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 @@ -30,13 +30,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; import { UserId } from "@bitwarden/user-core"; +import { Trial } from "@bitwarden/web-vault/app/billing/trial-initiation/trial-billing-step/trial-billing-step.service"; -import { - OrganizationCreatedEvent, - SubscriptionProduct, - TrialOrganizationType, -} from "../../../billing/accounts/trial-initiation/trial-billing-step.component"; import { RouterService } from "../../../core/router.service"; +import { OrganizationCreatedEvent } from "../trial-billing-step/trial-billing-step.component"; import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component"; export type InitiationPath = @@ -95,7 +92,6 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { }); private destroy$ = new Subject(); - protected readonly SubscriptionProduct = SubscriptionProduct; protected readonly ProductType = ProductType; protected trialPaymentOptional$ = this.configService.getFeatureFlag$( FeatureFlag.TrialPaymentOptional, @@ -338,14 +334,6 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { } } - get trialOrganizationType(): TrialOrganizationType | null { - if (this.productTier === ProductTierType.Free) { - return null; - } - - return this.productTier; - } - readonly showBillingStep$ = this.trialPaymentOptional$.pipe( map((trialPaymentOptional) => { return ( @@ -434,4 +422,26 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { return null; }); } + + get trial(): Trial { + const product = + this.product === ProductType.PasswordManager ? "passwordManager" : "secretsManager"; + + const tier = + this.productTier === ProductTierType.Families + ? "families" + : this.productTier === ProductTierType.Teams + ? "teams" + : "enterprise"; + + return { + organization: { + name: this.orgInfoFormGroup.value.name!, + email: this.orgInfoFormGroup.value.billingEmail!, + }, + product, + tier, + length: this.trialLength, + }; + } } diff --git a/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.html b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.html new file mode 100644 index 00000000000..51b7f0c7117 --- /dev/null +++ b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.html @@ -0,0 +1,87 @@ +@if (!(prices$ | async)) { + +} @else { + @let prices = prices$ | async; +
+
+ +
+

{{ "billingPlanLabel" | i18n }}

+ +
+ + + {{ "annual" | i18n }} - + {{ prices.annually | currency: "$" }} + /{{ "yr" | i18n }} + + +
+ @if (prices.monthly) { +
+ + + {{ "monthly" | i18n }} - + {{ prices.monthly | currency: "$" }} + /{{ "monthAbbr" | i18n }} + + +
+ } +
+
+ +
+

{{ "paymentType" | i18n }}

+ + + + @if (trial().length === 0) { + @let label = + trial().product === "passwordManager" + ? "passwordManagerPlanPrice" + : "secretsManagerPlanPrice"; +
+ @let selectionTaxAmounts = selectionCosts$ | async; +
+ {{ label | i18n }}: {{ selectionPrice$ | async | currency: "USD $" }} +
+ {{ "estimatedTax" | i18n }}: + {{ selectionTaxAmounts.tax | currency: "USD $" }} +
+
+
+

+ {{ "total" | i18n }}: + @let interval = formGroup.value.cadence === "annually" ? "year" : "month"; + {{ selectionTaxAmounts.total | currency: "USD $" }}/{{ interval | i18n }} +

+
+ } +
+ +
+ + +
+
+
+} + + + + {{ "loading" | i18n }} + diff --git a/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.ts b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.ts new file mode 100644 index 00000000000..0f185564c2e --- /dev/null +++ b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.ts @@ -0,0 +1,160 @@ +import { Component, input, OnDestroy, OnInit, output, ViewChild } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; +import { + combineLatest, + debounceTime, + filter, + map, + Observable, + shareReplay, + startWith, + switchMap, + Subject, + firstValueFrom, +} from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ToastService } from "@bitwarden/components"; +import { TaxClient } from "@bitwarden/web-vault/app/billing/clients"; +import { + BillingAddressControls, + EnterBillingAddressComponent, + EnterPaymentMethodComponent, +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { + Cadence, + Cadences, + Prices, + Trial, + TrialBillingStepService, +} from "@bitwarden/web-vault/app/billing/trial-initiation/trial-billing-step/trial-billing-step.service"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +export interface OrganizationCreatedEvent { + organizationId: string; + planDescription: string; +} + +@Component({ + selector: "app-trial-billing-step", + templateUrl: "./trial-billing-step.component.html", + imports: [EnterPaymentMethodComponent, EnterBillingAddressComponent, SharedModule], + providers: [TaxClient, TrialBillingStepService], +}) +export class TrialBillingStepComponent implements OnInit, OnDestroy { + @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; + + protected trial = input.required(); + protected steppedBack = output(); + protected organizationCreated = output(); + + private destroy$ = new Subject(); + + protected prices$!: Observable; + + protected selectionPrice$!: Observable; + protected selectionCosts$!: Observable<{ + tax: number; + total: number; + }>; + protected selectionDescription$!: Observable; + + protected formGroup = new FormGroup({ + cadence: new FormControl(Cadences.Annually, { + nonNullable: true, + }), + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), + }); + + constructor( + private i18nService: I18nService, + private toastService: ToastService, + private trialBillingStepService: TrialBillingStepService, + ) {} + + async ngOnInit() { + const { product, tier } = this.trial(); + this.prices$ = this.trialBillingStepService.getPrices$(product, tier); + + const cadenceChanged = this.formGroup.controls.cadence.valueChanges.pipe( + startWith(Cadences.Annually), + ); + + this.selectionPrice$ = combineLatest([this.prices$, cadenceChanged]).pipe( + map(([prices, cadence]) => prices[cadence]), + filter((price): price is number => !!price), + ); + + this.selectionCosts$ = combineLatest([ + cadenceChanged, + this.formGroup.controls.billingAddress.valueChanges.pipe( + startWith(this.formGroup.controls.billingAddress.value), + filter( + (billingAddress): billingAddress is BillingAddressControls => + !!billingAddress.country && !!billingAddress.postalCode, + ), + ), + ]).pipe( + debounceTime(500), + switchMap(([cadence, billingAddress]) => + this.trialBillingStepService.getCosts(product, tier, cadence, billingAddress), + ), + startWith({ + tax: 0, + total: 0, + }), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + this.selectionDescription$ = combineLatest([this.selectionPrice$, cadenceChanged]).pipe( + map(([price, cadence]) => { + switch (cadence) { + case Cadences.Annually: + return `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`; + case Cadences.Monthly: + return `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`; + } + }), + ); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + submit = async (): Promise => { + this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { + return; + } + + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + if (!paymentMethod) { + return; + } + + const billingAddress = this.formGroup.controls.billingAddress.getRawValue(); + + const organization = await this.trialBillingStepService.startTrial( + this.trial(), + this.formGroup.value.cadence!, + billingAddress, + paymentMethod, + ); + + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("organizationCreated"), + message: this.i18nService.t("organizationReadyToGo"), + }); + + this.organizationCreated.emit({ + organizationId: organization.id, + planDescription: await firstValueFrom(this.selectionDescription$), + }); + }; + + protected stepBack = () => this.steppedBack.emit(); +} diff --git a/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.service.ts b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.service.ts new file mode 100644 index 00000000000..9e4f45ede92 --- /dev/null +++ b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.service.ts @@ -0,0 +1,209 @@ +import { Injectable } from "@angular/core"; +import { firstValueFrom, from, map, shareReplay } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { + OrganizationBillingServiceAbstraction, + SubscriptionInformation, +} from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums"; +import { TaxClient } from "@bitwarden/web-vault/app/billing/clients"; +import { + BillingAddressControls, + getBillingAddressFromControls, +} from "@bitwarden/web-vault/app/billing/payment/components"; +import { + tokenizablePaymentMethodToLegacyEnum, + TokenizedPaymentMethod, +} from "@bitwarden/web-vault/app/billing/payment/types"; + +export const Tiers = { + Families: "families", + Teams: "teams", + Enterprise: "enterprise", +} as const; + +export const Cadences = { + Annually: "annually", + Monthly: "monthly", +} as const; + +export const Products = { + PasswordManager: "passwordManager", + SecretsManager: "secretsManager", +} as const; + +export type Tier = (typeof Tiers)[keyof typeof Tiers]; +export type Cadence = (typeof Cadences)[keyof typeof Cadences]; +export type Product = (typeof Products)[keyof typeof Products]; + +export type Prices = { + [Cadences.Annually]: number; + [Cadences.Monthly]?: number; +}; + +export interface Trial { + organization: { + name: string; + email: string; + }; + product: Product; + tier: Tier; + length: number; +} + +@Injectable() +export class TrialBillingStepService { + constructor( + private accountService: AccountService, + private apiService: ApiService, + private organizationBillingService: OrganizationBillingServiceAbstraction, + private taxClient: TaxClient, + ) {} + + private plans$ = from(this.apiService.getPlans()).pipe( + shareReplay({ bufferSize: 1, refCount: true }), + ); + + getPrices$ = (product: Product, tier: Tier) => + this.plans$.pipe( + map((plans) => { + switch (tier) { + case "families": { + const annually = plans.data.find((plan) => plan.type === PlanType.FamiliesAnnually); + return { + annually: annually!.PasswordManager.basePrice, + }; + } + case "teams": + case "enterprise": { + const annually = plans.data.find( + (plan) => + plan.type === + (tier === "teams" ? PlanType.TeamsAnnually : PlanType.EnterpriseAnnually), + ); + const monthly = plans.data.find( + (plan) => + plan.type === + (tier === "teams" ? PlanType.TeamsMonthly : PlanType.EnterpriseMonthly), + ); + switch (product) { + case "passwordManager": { + return { + annually: annually!.PasswordManager.seatPrice, + monthly: monthly!.PasswordManager.seatPrice, + }; + } + case "secretsManager": { + return { + annually: annually!.SecretsManager.seatPrice, + monthly: monthly!.SecretsManager.seatPrice, + }; + } + } + } + } + }), + ); + + getCosts = async ( + product: Product, + tier: Tier, + cadence: Cadence, + billingAddressControls: BillingAddressControls, + ): Promise<{ + tax: number; + total: number; + }> => { + const billingAddress = getBillingAddressFromControls(billingAddressControls); + return await this.taxClient.previewTaxForOrganizationSubscriptionPurchase( + { + tier, + cadence, + passwordManager: { + seats: 1, + additionalStorage: 0, + sponsored: false, + }, + secretsManager: + product === "secretsManager" + ? { + seats: 1, + additionalServiceAccounts: 0, + standalone: true, + } + : undefined, + }, + billingAddress, + ); + }; + + startTrial = async ( + trial: Trial, + cadence: Cadence, + billingAddress: BillingAddressControls, + paymentMethod: TokenizedPaymentMethod, + ): Promise => { + const getPlanType = async (tier: Tier, cadence: Cadence) => { + const plans = await firstValueFrom(this.plans$); + switch (tier) { + case "families": + return plans.data.find((plan) => plan.type === PlanType.FamiliesAnnually)!.type; + case "teams": + return plans.data.find( + (plan) => + plan.type === + (cadence === "annually" ? PlanType.TeamsAnnually : PlanType.TeamsMonthly), + )!.type; + case "enterprise": + return plans.data.find( + (plan) => + plan.type === + (cadence === "annually" ? PlanType.EnterpriseAnnually : PlanType.EnterpriseMonthly), + )!.type; + } + }; + + const legacyPaymentMethod: [string, PaymentMethodType] = [ + paymentMethod.token, + tokenizablePaymentMethodToLegacyEnum(paymentMethod.type), + ]; + const planType = await getPlanType(trial.tier, cadence); + + const request: SubscriptionInformation = { + organization: { + name: trial.organization.name, + billingEmail: trial.organization.email, + initiationPath: + trial.product === "passwordManager" + ? "Password Manager trial from marketing website" + : "Secrets Manager trial from marketing website", + }, + plan: + trial.product === "passwordManager" + ? { type: planType, passwordManagerSeats: 1 } + : { + type: planType, + passwordManagerSeats: 1, + subscribeToSecretsManager: true, + isFromSecretsManagerTrial: true, + secretsManagerSeats: 1, + }, + payment: { + paymentMethod: legacyPaymentMethod, + billing: { + country: billingAddress.country, + postalCode: billingAddress.postalCode, + taxId: billingAddress.taxId ?? undefined, + }, + skipTrial: trial.length === 0, + }, + }; + + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + return await this.organizationBillingService.purchaseSubscription(request, activeUserId); + }; +} diff --git a/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts b/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts index 06e1cce7f23..cd393f0dd5e 100644 --- a/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts +++ b/apps/web/src/app/billing/trial-initiation/trial-initiation.module.ts @@ -6,11 +6,11 @@ import { InputPasswordComponent } from "@bitwarden/auth/angular"; import { FormFieldModule } from "@bitwarden/components"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; -import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component"; import { SharedModule } from "../../shared"; import { CompleteTrialInitiationComponent } from "./complete-trial-initiation/complete-trial-initiation.component"; import { ConfirmationDetailsComponent } from "./confirmation-details.component"; +import { TrialBillingStepComponent } from "./trial-billing-step/trial-billing-step.component"; import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module"; @NgModule({ diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 7b1e598a77e..05a7f5aa64c 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -559,9 +559,12 @@ export class EventService { humanReadableMsg = this.i18nService.t("editedSecretWithId", this.getShortId(ev.secretId)); break; case EventType.Project_Retrieved: - msg = this.i18nService.t("accessedProjectWithId", this.formatProjectId(ev, options)); + msg = this.i18nService.t( + "accessedProjectWithIdentifier", + this.formatProjectId(ev, options), + ); humanReadableMsg = this.i18nService.t( - "accessedProjectWithId", + "accessedProjectWithIdentifier", this.getShortId(ev.projectId), ); break; @@ -583,6 +586,74 @@ export class EventService { msg = this.i18nService.t("editedProjectWithId", this.formatProjectId(ev, options)); humanReadableMsg = this.i18nService.t("editedProjectWithId", this.getShortId(ev.projectId)); break; + case EventType.ServiceAccount_UserAdded: + msg = this.i18nService.t( + "addedUserToServiceAccountWithId", + this.formatUserId(ev, options), + this.formatServiceAccountId(ev, options), + ); + humanReadableMsg = this.i18nService.t( + "addedUserToServiceAccountWithId", + this.formatUserId(ev, options), + this.formatServiceAccountId(ev, options), + ); + break; + case EventType.ServiceAccount_UserRemoved: + msg = this.i18nService.t( + "removedUserToServiceAccountWithId", + this.formatUserId(ev, options), + this.formatServiceAccountId(ev, options), + ); + humanReadableMsg = this.i18nService.t( + "removedUserToServiceAccountWithId", + this.formatUserId(ev, options), + this.formatServiceAccountId(ev, options), + ); + break; + case EventType.ServiceAccount_GroupRemoved: + msg = this.i18nService.t( + "removedGroupFromServiceAccountWithId", + this.formatGroupId(ev), + this.formatServiceAccountId(ev, options), + ); + humanReadableMsg = this.i18nService.t( + "removedGroupFromServiceAccountWithId", + this.formatGroupId(ev), + this.formatServiceAccountId(ev, options), + ); + break; + case EventType.ServiceAccount_GroupAdded: + msg = this.i18nService.t( + "addedGroupToServiceAccountId", + this.formatGroupId(ev), + this.formatServiceAccountId(ev, options), + ); + humanReadableMsg = this.i18nService.t( + "addedGroupToServiceAccountId", + this.formatGroupId(ev), + this.formatServiceAccountId(ev, options), + ); + break; + case EventType.ServiceAccount_Created: + msg = this.i18nService.t( + "serviceAccountCreatedWithId", + this.formatServiceAccountId(ev, options), + ); + humanReadableMsg = this.i18nService.t( + "serviceAccountCreatedWithId", + this.formatServiceAccountId(ev, options), + ); + break; + case EventType.ServiceAccount_Deleted: + msg = this.i18nService.t( + "serviceAccountDeletedWithId", + this.formatServiceAccountId(ev, options), + ); + humanReadableMsg = this.i18nService.t( + "serviceAccountDeletedWithId", + this.formatServiceAccountId(ev, options), + ); + break; default: break; } @@ -757,6 +828,35 @@ export class EventService { return a.outerHTML; } + formatServiceAccountId(ev: EventResponse, options: EventOptions): string { + const shortId = this.getShortId(ev.grantedServiceAccountId); + if (options.disableLink) { + return shortId; + } + const a = this.makeAnchor(shortId); + a.setAttribute( + "href", + "#/sm/" + + ev.organizationId + + "/machine-accounts?search=" + + shortId + + "&viewEvents=" + + ev.grantedServiceAccountId + + "&type=all", + ); + return a.outerHTML; + } + + formatUserId(ev: EventResponse, options: EventOptions): string { + const shortId = this.getShortId(ev.userId); + if (options.disableLink) { + return shortId; + } + const a = this.makeAnchor(shortId); + a.setAttribute("href", "#/organizations/" + ev.organizationId + "/members?search=" + shortId); + return a.outerHTML; + } + formatProjectId(ev: EventResponse, options: EventOptions): string { const shortId = this.getShortId(ev.projectId); if (options.disableLink) { diff --git a/apps/web/src/app/core/router.service.ts b/apps/web/src/app/core/router.service.ts index 603c171e95b..7a2e53a374e 100644 --- a/apps/web/src/app/core/router.service.ts +++ b/apps/web/src/app/core/router.service.ts @@ -75,9 +75,7 @@ export class RouterService { const titleId: string = child?.snapshot?.data?.titleId; const rawTitle: string = child?.snapshot?.data?.title; - // TODO: Eslint upgrade. Please resolve this since the ?? does nothing - // eslint-disable-next-line no-constant-binary-expression - const updateUrl = !child?.snapshot?.data?.doNotSaveUrl ?? true; + const updateUrl = !child?.snapshot?.data?.doNotSaveUrl; if (titleId != null || rawTitle != null) { const newTitle = rawTitle != null ? rawTitle : i18nService.t(titleId); diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts index 1a4141c4d68..bf2a528e723 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts @@ -89,6 +89,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple private async isPasswordExposed(cv: CipherView): Promise { const { login } = cv; + if (login.password == null) { + return null; + } return await this.auditService.passwordLeaked(login.password).then((exposedCount) => { if (exposedCount > 0) { return { ...cv, exposedXTimes: exposedCount } as ReportResult; 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 ba86e9fed28..95acf4447e9 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 @@ -87,7 +87,7 @@ export class ProductSwitcherService { startWith(null), // Start with a null event to trigger the initial combineLatest filter((e) => e instanceof NavigationEnd || e instanceof NavigationStart || e === null), ), - ]).pipe(map(() => null)); + ]).pipe(map((): any => null)); constructor( private organizationService: OrganizationService, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 9ac628752b6..7ffe69b7ee6 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -10,6 +10,7 @@ import { unauthGuardFn, activeAuthGuard, } from "@bitwarden/angular/auth/guards"; +import { LoginViaWebAuthnComponent } from "@bitwarden/angular/auth/login-via-webauthn/login-via-webauthn.component"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; import { @@ -17,6 +18,7 @@ import { RegistrationUserAddIcon, TwoFactorTimeoutIcon, TwoFactorAuthEmailIcon, + TwoFactorAuthSecurityKeyIcon, UserLockIcon, VaultIcon, SsoKeyIcon, @@ -49,7 +51,6 @@ import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/ import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { deepLinkGuard } from "./auth/guards/deep-link/deep-link.guard"; -import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component"; import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; @@ -106,11 +107,6 @@ const routes: Routes = [ children: [], // Children lets us have an empty component. canActivate: [redirectGuard()], // Redirects either to vault, login, or lock page. }, - { - path: "login-with-passkey", - component: LoginViaWebAuthnComponent, - data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties, - }, { path: "verify-email", component: VerifyEmailTokenComponent }, { path: "accept-organization", @@ -140,6 +136,28 @@ const routes: Routes = [ path: "", component: AnonLayoutWrapperComponent, children: [ + { + path: "login-with-passkey", + canActivate: [unauthGuardFn()], + data: { + pageIcon: TwoFactorAuthSecurityKeyIcon, + titleId: "logInWithPasskey", + pageTitle: { + key: "logInWithPasskey", + }, + pageSubtitle: { + key: "readingPasskeyLoadingInfo", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaWebAuthnComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, { path: "signup", canActivate: [unauthGuardFn()], diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index 4e04910246f..ce1b45a9e47 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -1,7 +1,6 @@ import { NgModule } from "@angular/core"; import { AuthModule } from "./auth"; -import { LoginModule } from "./auth/login/login.module"; import { TrialInitiationModule } from "./billing/trial-initiation/trial-initiation.module"; import { HeaderModule } from "./layouts/header/header.module"; import { SharedModule } from "./shared"; @@ -21,7 +20,6 @@ import "./shared/locales"; TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, - LoginModule, AuthModule, AccessComponent, ], @@ -31,7 +29,6 @@ import "./shared/locales"; TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, - LoginModule, AccessComponent, ], bootstrap: [], diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html index 41ee1b4707e..09bd38c8517 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html @@ -29,15 +29,32 @@
-
+
-

{{ "bitwardenExtensionInstalled" | i18n }}

+

+ {{ + (state === SetupExtensionState.Success + ? "bitwardenExtensionInstalled" + : "openTheBitwardenExtension" + ) | i18n + }} +

-

{{ "openExtensionToAutofill" | i18n }}

+

+ {{ + (state === SetupExtensionState.Success + ? "openExtensionToAutofill" + : "bitwardenExtensionInstalledOpenExtension" + ) | i18n + }} +

diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts index 3be8251b1d7..f755c83832f 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts @@ -93,14 +93,9 @@ describe("SetupExtensionComponent", () => { }); describe("extensionInstalled$", () => { - it("redirects the user to the vault when the first emitted value is true", () => { - extensionInstalled$.next(true); - - expect(navigate).toHaveBeenCalledWith(["/vault"]); - }); - describe("success state", () => { beforeEach(() => { + update.mockClear(); // avoid initial redirect extensionInstalled$.next(false); diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts index 558f0eb06c9..a04c529004c 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts @@ -36,6 +36,7 @@ export const SetupExtensionState = { Loading: "loading", NeedsExtension: "needs-extension", Success: "success", + AlreadyInstalled: "already-installed", ManualOpen: "manual-open", } as const; @@ -99,9 +100,10 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { this.webBrowserExtensionInteractionService.extensionInstalled$ .pipe(takeUntilDestroyed(this.destroyRef), startWith(null), pairwise()) .subscribe(([previousState, currentState]) => { - // Initial state transitioned to extension installed, redirect the user + // User landed on the page and the extension is already installed, show already installed state if (previousState === null && currentState) { - void this.router.navigate(["/vault"]); + void this.dismissExtensionPage(); + this.state = SetupExtensionState.AlreadyInstalled; } // Extension was not installed and now it is, show success state diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index c1955e56903..e351b9f46ce 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -13,7 +13,12 @@ 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 0e69ea3e567..0b6c8423221 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 @@ -2,7 +2,12 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { CollectionAdminView, Unassigned, CollectionView } from "@bitwarden/admin-console/common"; +import { + CollectionAdminView, + Unassigned, + CollectionView, + CollectionTypes, +} from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; @@ -26,6 +31,7 @@ export class VaultCollectionRowComponent { protected RowHeightClass = RowHeightClass; protected Unassigned = "unassigned"; protected CollectionPermission = CollectionPermission; + protected DefaultCollectionType = CollectionTypes.DefaultUserCollection; @Input() disabled: boolean; @Input() collection: CollectionView; 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 a5bcb915713..82ddda66110 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 @@ -17,6 +17,7 @@ import { CipherViewLikeUtils, } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { SortDirection, TableDataSource } from "@bitwarden/components"; +import { OrganizationId } from "@bitwarden/sdk-internal"; import { GroupView } from "../../../admin-console/organizations/core"; @@ -579,7 +580,7 @@ export class VaultItemsComponent { .every(({ cipher }) => cipher?.edit && cipher?.viewPassword); } - private getUniqueOrganizationIds(): Set { + private getUniqueOrganizationIds(): Set { return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? [])); } diff --git a/apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts index 14f6763dbf9..7520451df34 100644 --- a/apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts +++ b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts @@ -82,13 +82,6 @@ describe("setupExtensionRedirectGuard", () => { expect(await setupExtensionGuard()).toBe(true); }); - it("returns `true` when the user has the extension installed", async () => { - state$.next(false); - extensionInstalled$.next(true); - - expect(await setupExtensionGuard()).toBe(true); - }); - it('redirects the user to "/setup-extension" when all criteria do not pass', async () => { state$.next(false); extensionInstalled$.next(false); diff --git a/apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts index 5355ebf7232..0c9e31ae77f 100644 --- a/apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts +++ b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts @@ -11,8 +11,6 @@ import { UserKeyDefinition, } from "@bitwarden/common/platform/state"; -import { WebBrowserInteractionService } from "../services/web-browser-interaction.service"; - export const SETUP_EXTENSION_DISMISSED = new UserKeyDefinition( SETUP_EXTENSION_DISMISSED_DISK, "setupExtensionDismissed", @@ -27,7 +25,6 @@ export const setupExtensionRedirectGuard: CanActivateFn = async () => { const accountService = inject(AccountService); const vaultProfileService = inject(VaultProfileService); const stateProvider = inject(StateProvider); - const webBrowserInteractionService = inject(WebBrowserInteractionService); const isMobile = Utils.isMobileBrowser; @@ -43,10 +40,6 @@ export const setupExtensionRedirectGuard: CanActivateFn = async () => { return router.createUrlTree(["/login"]); } - const hasExtensionInstalledPromise = firstValueFrom( - webBrowserInteractionService.extensionInstalled$, - ); - const dismissedExtensionPage = await firstValueFrom( stateProvider .getUser(currentAcct.id, SETUP_EXTENSION_DISMISSED) @@ -66,13 +59,6 @@ export const setupExtensionRedirectGuard: CanActivateFn = async () => { return true; } - // Checking for the extension is a more expensive operation, do it last to avoid unnecessary delays. - const hasExtensionInstalled = await hasExtensionInstalledPromise; - - if (hasExtensionInstalled) { - return true; - } - return router.createUrlTree(["/setup-extension"]); }; diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts index 89a3757e939..6b46cd89956 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts @@ -1,14 +1,8 @@ import { TestBed } from "@angular/core/testing"; import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs"; -import { - AuthRequestServiceAbstraction, - UserDecryptionOptions, - UserDecryptionOptionsServiceAbstraction, -} from "@bitwarden/auth/common"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; -import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DeviceType } from "@bitwarden/common/enums"; @@ -18,7 +12,6 @@ import { StateProvider } from "@bitwarden/common/platform/state"; import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { KdfConfigService, KdfType } from "@bitwarden/key-management"; import { PREMIUM_BANNER_REPROMPT_KEY, @@ -34,14 +27,9 @@ describe("VaultBannersService", () => { const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); const getEmailVerified = jest.fn().mockResolvedValue(true); const lastSync$ = new BehaviorSubject(null); - const userDecryptionOptions$ = new BehaviorSubject({ - hasMasterPassword: true, - }); - const kdfConfig$ = new BehaviorSubject({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600000 }); const accounts$ = new BehaviorSubject>({ [userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo, }); - const devices$ = new BehaviorSubject([]); const pendingAuthRequests$ = new BehaviorSubject>([]); beforeEach(() => { @@ -64,32 +52,14 @@ describe("VaultBannersService", () => { provide: StateProvider, useValue: fakeStateProvider, }, - { - provide: PlatformUtilsService, - useValue: { isSelfHost }, - }, { provide: AccountService, useValue: { accounts$ }, }, - { - provide: KdfConfigService, - useValue: { getKdfConfig$: () => kdfConfig$ }, - }, { provide: SyncService, useValue: { lastSync$: () => lastSync$ }, }, - { - provide: UserDecryptionOptionsServiceAbstraction, - useValue: { - userDecryptionOptionsById$: () => userDecryptionOptions$, - }, - }, - { - provide: DevicesServiceAbstraction, - useValue: { getDevices$: () => devices$ }, - }, { provide: AuthRequestServiceAbstraction, useValue: { getPendingAuthRequests$: () => pendingAuthRequests$ }, @@ -197,45 +167,6 @@ describe("VaultBannersService", () => { }); }); - describe("KDFSettings", () => { - beforeEach(async () => { - userDecryptionOptions$.next({ hasMasterPassword: true }); - kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 }); - }); - - it("shows low KDF iteration banner", async () => { - service = TestBed.inject(VaultBannersService); - - expect(await service.shouldShowLowKDFBanner(userId)).toBe(true); - }); - - it("does not show low KDF iteration banner if KDF type is not PBKDF2_SHA256", async () => { - kdfConfig$.next({ kdfType: KdfType.Argon2id, iterations: 600001 }); - - service = TestBed.inject(VaultBannersService); - - expect(await service.shouldShowLowKDFBanner(userId)).toBe(false); - }); - - it("does not show low KDF for iterations about 600,000", async () => { - kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 }); - - service = TestBed.inject(VaultBannersService); - - expect(await service.shouldShowLowKDFBanner(userId)).toBe(false); - }); - - it("dismisses low KDF iteration banner", async () => { - service = TestBed.inject(VaultBannersService); - - expect(await service.shouldShowLowKDFBanner(userId)).toBe(true); - - await service.dismissBanner(userId, VisibleVaultBanner.KDFSettings); - - expect(await service.shouldShowLowKDFBanner(userId)).toBe(false); - }); - }); - describe("OutdatedBrowser", () => { beforeEach(async () => { // Hardcode `MSIE` in userAgent string 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 c4396940998..1c53274d9d7 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 @@ -1,10 +1,7 @@ import { Injectable } from "@angular/core"; import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs"; -import { - AuthRequestServiceAbstraction, - UserDecryptionOptionsServiceAbstraction, -} from "@bitwarden/auth/common"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -18,10 +15,8 @@ import { import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; -import { PBKDF2KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management"; export const VisibleVaultBanner = { - KDFSettings: "kdf-settings", OutdatedBrowser: "outdated-browser", Premium: "premium", VerifyEmail: "verify-email", @@ -64,9 +59,7 @@ export class VaultBannersService { private stateProvider: StateProvider, private billingAccountProfileStateService: BillingAccountProfileStateService, private platformUtilsService: PlatformUtilsService, - private kdfConfigService: KdfConfigService, private syncService: SyncService, - private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, ) {} @@ -133,21 +126,6 @@ export class VaultBannersService { return needsVerification && !alreadyDismissed; } - /** Returns true when the low KDF iteration banner should be shown */ - async shouldShowLowKDFBanner(userId: UserId): Promise { - const hasLowKDF = ( - await firstValueFrom(this.userDecryptionOptionsService.userDecryptionOptionsById$(userId)) - )?.hasMasterPassword - ? await this.isLowKdfIteration(userId) - : false; - - const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes( - VisibleVaultBanner.KDFSettings, - ); - - return hasLowKDF && !alreadyDismissed; - } - /** Dismiss the given banner and perform any respective side effects */ async dismissBanner(userId: UserId, banner: SessionBanners): Promise { if (banner === VisibleVaultBanner.Premium) { @@ -221,13 +199,4 @@ 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.html b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html index d52ea9f61e6..44b2975ee19 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html @@ -25,18 +25,6 @@ - - {{ "lowKDFIterationsBanner" | i18n }} - - {{ "changeKDFSettings" | i18n }} - - - { shouldShowPremiumBanner$: jest.fn((userId: UserId) => premiumBanner$), shouldShowUpdateBrowserBanner: jest.fn(), shouldShowVerifyEmailBanner: jest.fn(), - shouldShowLowKDFBanner: jest.fn(), shouldShowPendingAuthRequestBanner: jest.fn((userId: UserId) => Promise.resolve(pendingAuthRequest$.value), ), @@ -48,7 +47,6 @@ describe("VaultBannersComponent", () => { messageSubject = new Subject<{ command: string }>(); bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false); bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false); - bannerService.shouldShowLowKDFBanner.mockResolvedValue(false); pendingAuthRequest$.next(false); premiumBanner$.next(false); @@ -137,11 +135,6 @@ describe("VaultBannersComponent", () => { method: bannerService.shouldShowVerifyEmailBanner, banner: VisibleVaultBanner.VerifyEmail, }, - { - name: "LowKDF", - method: bannerService.shouldShowLowKDFBanner, - banner: VisibleVaultBanner.KDFSettings, - }, ].forEach(({ name, method, banner }) => { describe(name, () => { beforeEach(async () => { 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 a16374f19b3..cd32eaf2858 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 @@ -5,8 +5,6 @@ import { filter, firstValueFrom, map, Observable, switchMap } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; import { BannerModule } from "@bitwarden/components"; @@ -41,7 +39,6 @@ export class VaultBannersComponent implements OnInit { private router: Router, private accountService: AccountService, private messageListener: MessageListener, - private configService: ConfigService, ) { this.premiumBannerVisible$ = this.activeUserId$.pipe( filter((userId): userId is UserId => userId != null), @@ -75,16 +72,12 @@ export class VaultBannersComponent implements OnInit { } async navigateToPaymentMethod(organizationId: string): Promise { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; const navigationExtras = { state: { launchPaymentModalAutomatically: true }, }; await this.router.navigate( - ["organizations", organizationId, "billing", route], + ["organizations", organizationId, "billing", "payment-details"], navigationExtras, ); } @@ -100,14 +93,12 @@ export class VaultBannersComponent implements OnInit { const showBrowserOutdated = await this.vaultBannerService.shouldShowUpdateBrowserBanner(activeUserId); const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner(activeUserId); - const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner(activeUserId); const showPendingAuthRequest = await this.vaultBannerService.shouldShowPendingAuthRequestBanner(activeUserId); this.visibleBanners = [ showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null, showVerifyEmail ? VisibleVaultBanner.VerifyEmail : null, - showLowKdf ? VisibleVaultBanner.KDFSettings : null, showPendingAuthRequest ? VisibleVaultBanner.PendingAuthRequest : null, ].filter((banner) => banner !== null); } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 6feaa52d190..278ae62aaa6 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -19,12 +19,12 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { CipherArchiveService } from "@bitwarden/vault"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { VaultFilterService } from "../services/abstractions/vault-filter.service"; @@ -208,15 +208,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy { } applyOrganizationFilter = async (orgNode: TreeNode): Promise => { - if (!orgNode?.node.enabled) { - this.toastService.showToast({ - variant: "error", - message: this.i18nService.t("disabledOrganizationFilterError"), - }); - await firstValueFrom( - this.organizationWarningsService.showInactiveSubscriptionDialog$(orgNode.node), - ); - } const filter = this.activeFilter; if (orgNode?.node.id === "AllVaults") { filter.resetOrganization(); @@ -433,7 +424,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { [ { id: "archive", - name: this.i18nService.t("archive"), + name: this.i18nService.t("archiveNoun"), type: "archive", icon: "bwi-archive", }, 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 5897ea8c2ce..1f27773c467 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 @@ -12,7 +12,11 @@ import { switchMap, } from "rxjs"; -import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { + CollectionService, + CollectionTypes, + CollectionView, +} from "@bitwarden/admin-console/common"; import { sortDefaultCollections } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.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"; @@ -257,7 +261,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { const collectionCopy = cloneCollection( new CollectionView({ ...c, name: c.name }), ) as CollectionFilter; - collectionCopy.icon = "bwi-collection-shared"; + collectionCopy.icon = + c.type === CollectionTypes.DefaultUserCollection ? "bwi-user" : "bwi-collection-shared"; const parts = c.name ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : []; ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter); } diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 63e6d391b42..929a8d07881 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -7,6 +7,7 @@ import { Unassigned, CollectionView, CollectionAdminService, + CollectionTypes, } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -140,7 +141,7 @@ export class VaultHeaderComponent { } if (this.filter.type === "archive") { - return this.i18nService.t("archive"); + return this.i18nService.t("archiveNoun"); } const activeOrganization = this.activeOrganization; @@ -152,9 +153,12 @@ export class VaultHeaderComponent { } protected get icon() { - return this.filter?.collectionId && this.filter.collectionId !== All - ? "bwi-collection-shared" - : ""; + if (!this.filter?.collectionId || this.filter.collectionId === All) { + return ""; + } + return this.collection?.node.type === CollectionTypes.DefaultUserCollection + ? "bwi-user" + : "bwi-collection-shared"; } /** diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index 958cf655d17..68219173deb 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -68,19 +68,20 @@ class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start" *ngIf="isEmpty && !performingInitialLoad" > - -
{{ "noItemsInArchive" | i18n }}
-

- {{ "archivedItemsDescription" | i18n }} + +

+ {{ (emptyState$ | async)?.title | i18n }} +
+

+ {{ (emptyState$ | async)?.description | i18n }}

-
{{ "noItemsInList" | i18n }}
-
- -
- - 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 deleted file mode 100644 index d60a6313c67..00000000000 --- a/apps/web/src/app/vault/individual-vault/view.component.spec.ts +++ /dev/null @@ -1,148 +0,0 @@ -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"; -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 { 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { TaskService } from "@bitwarden/common/vault/tasks"; -import { DIALOG_DATA, DialogRef, DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; -import { ChangeLoginPasswordService } from "@bitwarden/vault"; - -import { ViewCipherDialogParams, ViewCipherDialogResult, ViewComponent } from "./view.component"; - -describe("ViewComponent", () => { - let component: ViewComponent; - let fixture: ComponentFixture; - - const mockCipher: CipherView = { - id: "cipher-id", - type: 1, - organizationId: "org-id", - isDeleted: false, - } as CipherView; - - const mockOrganization: Organization = { - id: "org-id", - name: "Test Organization", - } as Organization; - - const mockParams: ViewCipherDialogParams = { - cipher: mockCipher, - }; - const userId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(userId); - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ViewComponent], - providers: [ - { provide: DIALOG_DATA, useValue: mockParams }, - { provide: DialogRef, useValue: mock() }, - { provide: I18nService, useValue: { t: jest.fn().mockReturnValue("login") } }, - { provide: DialogService, useValue: mock() }, - { provide: CipherService, useValue: mock() }, - { provide: ToastService, useValue: mock() }, - { provide: MessagingService, useValue: mock() }, - { - provide: AccountService, - useValue: accountService, - }, - { provide: LogService, useValue: mock() }, - { - provide: OrganizationService, - useValue: { organizations$: jest.fn().mockReturnValue(of([mockOrganization])) }, - }, - { provide: CollectionService, useValue: mock() }, - { provide: FolderService, useValue: mock() }, - { provide: KeyService, useValue: mock() }, - { - provide: BillingAccountProfileStateService, - useValue: mock(), - }, - { provide: ConfigService, useValue: mock() }, - { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) }, - { - provide: CipherAuthorizationService, - useValue: { - canDeleteCipher$: jest.fn().mockReturnValue(true), - }, - }, - { provide: TaskService, useValue: mock() }, - ], - }) - .overrideComponent(ViewComponent, { - remove: { - providers: [ - { provide: PlatformUtilsService, useValue: PlatformUtilsService }, - { - provide: ChangeLoginPasswordService, - useValue: ChangeLoginPasswordService, - }, - ], - }, - add: { - providers: [ - { provide: PlatformUtilsService, useValue: mock() }, - { - provide: ChangeLoginPasswordService, - useValue: mock(), - }, - ], - }, - }) - .compileComponents(); - - fixture = TestBed.createComponent(ViewComponent); - component = fixture.componentInstance; - component.params = mockParams; - component.cipher = mockCipher; - }); - - describe("ngOnInit", () => { - it("initializes the component with cipher and organization", async () => { - await component.ngOnInit(); - - expect(component.cipher).toEqual(mockCipher); - expect(component.organization).toEqual(mockOrganization); - }); - }); - - describe("edit", () => { - it("closes the dialog with the proper arguments", async () => { - const dialogRefCloseSpy = jest.spyOn(component["dialogRef"], "close"); - - await component.edit(); - - expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: ViewCipherDialogResult.Edited }); - }); - }); - - describe("delete", () => { - it("calls the delete method on delete and closes the dialog with the proper arguments", async () => { - const deleteSpy = jest.spyOn(component, "delete"); - const dialogRefCloseSpy = jest.spyOn(component["dialogRef"], "close"); - jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); - - await component.delete(); - - expect(deleteSpy).toHaveBeenCalled(); - expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: ViewCipherDialogResult.Deleted }); - }); - }); -}); diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts deleted file mode 100644 index 6de29f8e328..00000000000 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ /dev/null @@ -1,218 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { CommonModule } from "@angular/common"; -import { Component, EventEmitter, Inject, OnInit } from "@angular/core"; -import { firstValueFrom, map, Observable } from "rxjs"; - -import { CollectionView } from "@bitwarden/admin-console/common"; -import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.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 { 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 { 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"; -import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; -import { - DIALOG_DATA, - DialogRef, - DialogConfig, - AsyncActionsModule, - DialogModule, - DialogService, - ToastService, -} from "@bitwarden/components"; -import { CipherViewComponent } from "@bitwarden/vault"; - -import { SharedModule } from "../../shared/shared.module"; - -export interface ViewCipherDialogParams { - cipher: CipherView; - - /** - * Optional list of collections the cipher is assigned to. If none are provided, they will be loaded using the - * `CipherService` and the `collectionIds` property of the cipher. - */ - collections?: CollectionView[]; - - /** - * Optional collection ID used to know the collection filter selected. - */ - activeCollectionId?: CollectionId; - - /** - * If true, the edit button will be disabled in the dialog. - */ - disableEdit?: boolean; -} - -export const ViewCipherDialogResult = { - Edited: "edited", - Deleted: "deleted", - PremiumUpgrade: "premiumUpgrade", -} as const; - -type ViewCipherDialogResult = UnionOfValues; - -export interface ViewCipherDialogCloseResult { - action: ViewCipherDialogResult; -} - -/** - * Component for viewing a cipher, presented in a dialog. - * @deprecated Use the VaultItemDialogComponent instead. - */ -@Component({ - selector: "app-vault-view", - templateUrl: "view.component.html", - imports: [CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, SharedModule], - providers: [{ provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService }], -}) -export class ViewComponent implements OnInit { - cipher: CipherView; - collections?: CollectionView[]; - onDeletedCipher = new EventEmitter(); - cipherTypeString: string; - organization: Organization; - - canDeleteCipher$: Observable; - - constructor( - @Inject(DIALOG_DATA) public params: ViewCipherDialogParams, - private dialogRef: DialogRef, - private i18nService: I18nService, - private dialogService: DialogService, - private messagingService: MessagingService, - private logService: LogService, - private cipherService: CipherService, - private toastService: ToastService, - private organizationService: OrganizationService, - private cipherAuthorizationService: CipherAuthorizationService, - private accountService: AccountService, - ) {} - - /** - * Lifecycle hook for component initialization. - */ - async ngOnInit() { - 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 firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe( - map((organizations) => organizations.find((o) => o.id === this.cipher.organizationId)), - ), - ); - } - - this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher); - } - - /** - * Method to handle cipher deletion. Called when a user clicks the delete button. - */ - delete = async () => { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "deleteItem" }, - content: { - key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation", - }, - type: "warning", - }); - - if (!confirmed) { - return; - } - - try { - await this.deleteCipher(); - this.toastService.showToast({ - variant: "success", - title: this.i18nService.t("success"), - message: this.i18nService.t( - this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", - ), - }); - this.onDeletedCipher.emit(this.cipher); - this.messagingService.send( - this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher", - ); - } catch (e) { - this.logService.error(e); - } - - this.dialogRef.close({ action: ViewCipherDialogResult.Deleted }); - }; - - /** - * Helper method to delete cipher. - */ - protected async deleteCipher(): Promise { - const asAdmin = this.organization?.canEditAllCiphers; - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - if (this.cipher.isDeleted) { - await this.cipherService.deleteWithServer(this.cipher.id, userId, asAdmin); - } else { - await this.cipherService.softDeleteWithServer(this.cipher.id, userId, asAdmin); - } - } - - /** - * Method to handle cipher editing. Called when a user clicks the edit button. - */ - async edit(): Promise { - this.dialogRef.close({ action: ViewCipherDialogResult.Edited }); - } - - /** - * Method to get cipher view type string, used for the dialog title. - * E.g. "View login" or "View note". - * @returns The localized string for the cipher type - */ - getCipherViewTypeString(): string { - if (!this.cipher) { - return null; - } - - switch (this.cipher.type) { - case CipherType.Login: - return this.i18nService.t("viewItemHeaderLogin"); - case CipherType.SecureNote: - return this.i18nService.t("viewItemHeaderCard"); - case CipherType.Card: - return this.i18nService.t("viewItemHeaderIdentity"); - case CipherType.Identity: - return this.i18nService.t("viewItemHeaderNote"); - case CipherType.SshKey: - return this.i18nService.t("viewItemHeaderSshKey"); - default: - return null; - } - } -} - -/** - * Strongly typed helper to open a cipher view dialog - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - * @returns A reference to the opened dialog - */ -export function openViewCipherDialog( - dialogService: DialogService, - config: DialogConfig, -): DialogRef { - return dialogService.open(ViewComponent, config); -} diff --git a/apps/web/src/images/integrations/logo-datadog-color.svg b/apps/web/src/images/integrations/logo-datadog-color.svg new file mode 100644 index 00000000000..62e608cd544 --- /dev/null +++ b/apps/web/src/images/integrations/logo-datadog-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index fcfd90269f2..11195502289 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Ongeldige hoofwagwoord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Daar is geen items om te lys nie." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisasie is gedeaktiveer." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "Lisensie het verstryk." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO-identifiseerder" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Toegang geweiger. U het nie toestemming om hierdie blad te sien nie." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Groep/Gebruiker" }, - "lowKdfIterations": { - "message": "Lae KDF-iteraties" - }, - "updateLowKdfIterationsDesc": { - "message": "Werk u enkripsie-instellings by om aan die nuwe beveiligingsaanbevelings te voldoen en die beskerming van u rekening te verbeter." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Verander KDF-instellings" - }, "secureYourInfrastructure": { "message": "Beveilig u infrastruktuur" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index fd0214fa71d..7dfe8fe7cee 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "إنشاء عنصر تسجيل دخول جديد" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "التطبيقات الحساسة ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "الأعضاء المبلّغون ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "الأعضاء المعرضون للخطر" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "كلمة المرور الرئيسية غير صالحة" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "كلمة مرور الملف غير صالحة، الرجاء استخدام كلمة المرور التي أدخلتها عند إنشاء ملف التصدير." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "لا توجد أية عناصر في القائمة." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "ليس لديك الصلاحية لعرض جميع العناصر في هذه المجموعة." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "تم تعليق المؤسسة" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "معرف SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index c9d53bd9c52..e4a4149d9d6 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -59,8 +59,61 @@ "createNewLoginItem": { "message": "Yeni giriş elementi yarat" }, - "criticalApplicationsActivityDescription": { - "message": "Tətbiqləri kritik olaraq işarələsəniz, onlar burada nümayiş olunacaq." + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "Once you mark applications critical, they will display here." + }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } }, "criticalApplicationsWithCount": { "message": "Kritik tətbiqlər ($COUNT$)", @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Məlumatlandırılan üzvlər ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Riskli üzvlər" }, - "membersAtRiskActivityDescription": { - "message": "Kritik tətbiqlər üçün risk altındakı elementlərə düzəliş erişimi olan üzvlər" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ üzv riskdədir", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Yararsız ana parol" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Yararsız fayl parolu, lütfən xaricə köçürmə faylını yaradarkən daxil etdiyiniz parolu istifadə edin." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Sadalanacaq heç bir element yoxdur." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "Bu kolleksiyadakı bütün elementlərə baxmaq üçün icazəniz yoxdur." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Təşkilat sıradan çıxarıldı." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Fəaliyyəti dayandırılmış təşkilatlara erişilə bilməz. Lütfən kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Xidmət hesabları dayandırılmış təşkilatlarda yaradıla bilməz. Lütfən kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." }, - "disabledOrganizationFilterError": { - "message": "Fəaliyyəti dayandırılmış təşkilatlardakı elementlərə erişilə bilməz. Kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." - }, "licenseIsExpired": { "message": "Lisenziyanın vaxtı bitib." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifikatoru" }, - "ssoIdentifierHintPartOne": { - "message": "SSO ilə giriş etmələri üçün üzvlərinizi bu kimliklə təmin edin. Bu addımı ötürmək üçün, quraşdırın ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "SSO əlaqəsini kəs" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Erişim rədd edildi. Bu səhifəyə baxmaq üçün icazəniz yoxdur." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Bilinməyən sirr, bu sirrə erişmək üçün icazə istəməli ola bilərsiniz." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Bilinməyən layihə, bu layihəyə erişmək üçün icazə istəməli ola bilərsiniz." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Qrup/İstifadəçi" }, - "lowKdfIterations": { - "message": "Aşağı KDF iterasiyaları" - }, - "updateLowKdfIterationsDesc": { - "message": "Yeni güvənlik tövsiyələrini qarşılamaq və hesab qorumasını təkmilləşdirmək üçün şifrələmə ayarlarınızı güncəlləyin." - }, "kdfSettingsChangeLogoutWarning": { "message": "Davam etsəniz, bütün aktiv seanslardan çıxış edəcəksiniz. Təkrar giriş etməyiniz və əgər varsa iki addımlı girişi tamamlamağınız lazım olacaq. Veri itkisini önləmək üçün şifrələmə ayarlarınızı dəyişdirməzdən əvvəl seyfinizi xaricə köçürməyinizi tövsiyə edirik." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "$PROJECT_ID$ kimliyinə sahib bir layihəyə erişildi.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "$SECRET_ID$ identifikatoruna sahib bir layihəyə düzəliş edildi.", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "$PROJECT_ID$ identifikatoruna sahib bir layihə silindi.", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Təyin et" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Kolleksiyalara təyin et" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "İnteqrasiya saxlanılmadı. Lütfən daha sonra yenidən sınayın." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "İnteqrasiya silinmədi. Lütfən daha sonra yenidən sınayın." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Aşağı KDF iterasiyaları. Hesabınızın güvənliyini təkmilləşdirmək üçün iterasiyaları artırın." - }, - "changeKDFSettings": { - "message": "KDF ayarlarını dəyişdir" - }, "secureYourInfrastructure": { "message": "İnfrastrukturunuzu qoruyun" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Arxivdə axtar" }, - "archive": { - "message": "Arxiv" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "Arxivdə element yoxdur" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden uzantısı quraşdırıldı!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Giriş etmək üçün uzantını aç və avto-doldurmağa başla." }, diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 91eb4328810..8c6000c3f38 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Стварыць новы элемент запісу ўваходу" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Крытычныя праграмы ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Апавешчаныя ўдзельнікі ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Удзельнікі ў зоне рызыкі" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Памылковы асноўны пароль" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Памылковы пароль файла. Скарыстайцеся паролем, які вы ўводзілі пры стварэнні файла экспартавання." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "У спісе адсутнічаюць элементы." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "У вас няма правоў для прагляду ўсіх элементаў у гэтай калекцыі." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Арганізацыя адключана." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Доступ да элементаў у адключаных арганізацыях немагчымы. Звяжыце з уладальнікам арганізацыі для атрымання дапамогі." - }, "licenseIsExpired": { "message": "Ліцэнзія пратэрмінавана." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Ідэнтыфікатар SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Адлучыць SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Доступ забаронены. У вас не дастаткова правоў для прагляду гэтай старонкі." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Група/Карыстальнік" }, - "lowKdfIterations": { - "message": "Нізкае значэнне ітэрацыі KDF" - }, - "updateLowKdfIterationsDesc": { - "message": "Абнавіце свае налады шыфравання, каб адпавядаць новым рэкамендацыям бяспекі і палепшыць абарону ўліковага запісу." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 86136cb8f16..dd0c14b8f76 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Създаване на нов елемент за вписване" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% завършено", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ от $TOTAL$ задачи по сигурността за завършени", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Напредък по смяната на паролата" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Назначете задачи на членовете, за да следите напредъка" + }, + "onceYouReviewApplications": { + "message": "Когато прегледате приложенията и ги отбележите като важни, те ще се появят тук." + }, + "sendReminders": { + "message": "Изпращане на напомняния" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Когато отбележите някое приложение като важно, то ще се появи тук." }, + "viewAtRiskMembers": { + "message": "Преглед на членовете в риск" + }, + "viewAtRiskApplications": { + "message": "Преглед на приложенията в риск" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ от $TOTAL$ важни приложения са в риск, тъй като има пароли в риск", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Важни приложения ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ прилжения са в риск", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ пароли в риск", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Известени членове ($COUNT$)", "placeholders": { @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "Членове в риск" }, - "membersAtRiskActivityDescription": { - "message": "Членове с права за редактиране на елементи в риск за важни приложения" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Членове с достъп до елементи в риск за важни приложения" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "$COUNT$ членове в риск", "placeholders": { "count": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Грешна главна парола" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Грешна главна парола. Проверете дали е-пощата е правилна и дали акаунтът Ви е създаден в $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Неправилна парола за файла. Използвайте паролата, която сте въвели при създаването на изнесения файл." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Няма елементи за показване." }, + "noItemsInTrash": { + "message": "Няма елементи в кошчето" + }, + "noItemsInTrashDesc": { + "message": "Елементите, които изтривате, ще бъдат премествани тук и изтривани окончателно след 30 дни" + }, + "noItemsInVault": { + "message": "Няма елементи в трезора" + }, + "emptyVaultDescription": { + "message": "Трезорът може да пази не само паролите Ви. Тум можете да съхранявате сигурно данни за вписване, идентификационни данни, карти и бележки." + }, + "emptyFavorites": { + "message": "Все още нямате любими неща" + }, + "emptyFavoritesDesc": { + "message": "Добавете често ползваните неща в любимите си, за бърз достъп." + }, + "noSearchResults": { + "message": "Няма резултати от търсенето" + }, + "clearFiltersOrTryAnother": { + "message": "Изчистете филтрите или опитайте да търсите нещо друго" + }, "noPermissionToViewAllCollectionItems": { "message": "Нямате права да преглеждате всички елементи в тази колекция." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Организацията е изключена." }, + "organizationIsSuspended": { + "message": "Организацията е с преустановен достъп" + }, + "organizationIsSuspendedDesc": { + "message": "Записите в организации с преустановен достъп не са достъпни. Свържете се със собственика на организацията си за помощ." + }, "secretsAccessSuspended": { "message": "Изключените организации са недостъпни. Свържете се със собственика на организацията си за помощ." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "В изключени организации не могат да се създават сервизни акаунти. Свържете се със собственика на организацията си за помощ." }, - "disabledOrganizationFilterError": { - "message": "Записите в изключени организации не са достъпни. Свържете се със собственика на организацията си за помощ." - }, "licenseIsExpired": { "message": "Изтекъл лиценз." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Идентификатор за еднократна идентификация" }, - "ssoIdentifierHintPartOne": { - "message": "Дайте този идентификатор на членовете си, за да могат да се вписват чрез еднократно удостоверяване. Ако искате да пропуснете тази стъпка, настройте ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Дайте този идентификатор на членовете си, за да се вписват чрез еднократно удостоверяване. Членовете могат да пропуснат въвеждането та този идентификатор по време на еднократното удостоверяване, ако е настроен присвоен домейн. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Научете повече", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Прекъсване на еднократна идентификация" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Ще бъдат изнесени само записите от трезора свързан с $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Ще бъдат изнесени само записите от трезора свързан с $ORGANIZATION$. Моите колекции няма да бъдат включени.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Достъпът е отказан. Нямате право за преглед на тази страница." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Неизвестна тайна. Може да се нуждаете от правомощие за достъп до тази тайна." }, + "unknownServiceAccount": { + "message": "Неизвестен машинен акаунт. Може да се нуждаете от правомощие за достъп до този машинен акаунт." + }, "unknownProject": { "message": "Неизвестен проект. Може да се нуждаете от правомощие за достъп до този проект." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Група/потребител" }, - "lowKdfIterations": { - "message": "Малко повторения за KDF" - }, - "updateLowKdfIterationsDesc": { - "message": "Променете настройките си за шифроване, така че да отговарят на новите препоръки за сигурността и да подобрите защитата на регистрацията си." - }, "kdfSettingsChangeLogoutWarning": { "message": "Ако продължите, ще излезете от всички активни сесии. Ще трябва да се впишете отново и да извършите двустепенно удостоверяване, ако такова е настроено. Препоръчително е да изнесете трезора си преди да променяте настройките за шифроване, за да избегнете загуба на данни." }, @@ -8485,7 +8611,7 @@ } } }, - "accessedProjectWithId": { + "accessedProjectWithIdentifier": { "message": "Осъществен е достъп до проект с идентификатор: $PROJECT_ID$.", "placeholders": { "project_id": { @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Изтрит е машинен акаунт с идентификатор: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Редактиран е проект с идентификатор: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Добавен е потребител: $USER_ID$ към машинен акаунт с идентификатор: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Премахнат е потребител: $USER_ID$ от машинен акаунт с идентификатор: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Премахната е група: $GROUP_ID$ от машинен акаунт с идентификатор: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Създаден е машинен акаунт с идентификатор: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Създадена е група: $GROUP_ID$ към машинен акаунт с идентификатор: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Изтрит е машинен акаунт с идентификатор: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Изтрит е проект с идентификатор: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Свързване" }, + "assignTasks": { + "message": "Назначаване на задачи" + }, "assignToCollections": { "message": "Свързване с колекции" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Интеграцията не беше запазена. Опитайте отново по-късно." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Трябва да бъдете собственик на организацията, за да извършите това действие." + }, "failedToDeleteIntegration": { "message": "Интеграцията не беше изтрита. Опитайте отново по-късно." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Малък брой повторения за KDF. Увеличете броя повторения, за да подсилите защитата на регистрацията си." - }, - "changeKDFSettings": { - "message": "Промяна на настройките за KDF" - }, "secureYourInfrastructure": { "message": "Подсигурете инфраструктурата си" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Търсене в архива" }, - "archive": { - "message": "Архив" + "archiveNoun": { + "message": "Архив", + "description": "Noun" + }, + "archiveVerb": { + "message": "Архивиране", + "description": "Verb" }, "noItemsInArchive": { "message": "Няма елементи в архива" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Добавката на Битуорден е инсталирана!" }, + "openTheBitwardenExtension": { + "message": "Отварете добавката на Битуорден" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "Добавката на Битуорден е инсталирана! Отворете я, за да се впишете и да можете да се вписвате автоматично." + }, "openExtensionToAutofill": { "message": "Отворете добавката, за да се впишете и да можете да използвате автоматичното попълване." }, diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 53f4cc91ae1..a0d38221ef2 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "অবৈধ মূল পাসওয়ার্ড" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "তালিকার জন্য কোনও বস্তু নেই।" }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index f53fc830b7a..3f723a74e80 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index d9dfa7c1eac..90d69ca33ad 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Crea un nou element d'inici de sessió" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Aplicacions crítiques ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Membres notificats ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Contrasenya mestra no vàlida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "La contrasenya del fitxer no és vàlida. Utilitzeu la contrasenya que vau introduir quan vau crear el fitxer d'exportació." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "No hi ha cap element a llistar." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "No teniu permís per veure tots els elements en aquesta col·lecció." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "L'organització està inhabilitada." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "No es pot accedir a les organitzacions suspeses. Poseu-vos en contacte amb el propietari de la vostra organització per obtenir ajuda." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "No es poden crear comptes de servei en organitzacions suspeses. Poseu-vos en contacte amb el propietari de la vostra organització per obtenir ajuda." }, - "disabledOrganizationFilterError": { - "message": "No es pot accedir als elements de les organitzacions inhabilitades. Poseu-vos en contacte amb el propietari de la vostra organització per obtenir ajuda." - }, "licenseIsExpired": { "message": "Llicència caducada." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Identificador SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Proporcioneu aquest identificador als vostres membres per iniciar sessió amb SSO. Per evitar aquest pas, configureu-ho ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Desenllaça SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Accés denegat. No teniu permís per veure aquesta pàgina." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Grup/Usuari" }, - "lowKdfIterations": { - "message": "Iteracions de KDF baixes" - }, - "updateLowKdfIterationsDesc": { - "message": "Actualitzeu la configuració d'encriptació per complir les noves recomanacions de seguretat i millorar la protecció del compte." - }, "kdfSettingsChangeLogoutWarning": { "message": "Si continueu, tancareu totes les sessions actives. Haureu de tornar a iniciar la sessió i completar l'inici de sessió en dos passos. Recomanem que exporteu la caixa forta abans de canviar la configuració de xifratge per evitar la pèrdua de dades." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assigna" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assigna a col·leccions" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Iteracions baixes de KDF. Augmenta les iteracions per millorar la seguretat del teu compte." - }, - "changeKDFSettings": { - "message": "Canvia la configuració de KDF" - }, "secureYourInfrastructure": { "message": "Assegureu la vostra infraestructura" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 5837d159e92..62af2581a57 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Vytvořit novou položku přihlášení" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$ % dokončeno", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ z $TOTAL$ bezpečnostních úkolů dokončeno", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Probíhá změna hesla" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Přiřadit úkoly členů ke sledování průběhu" + }, + "onceYouReviewApplications": { + "message": "Jakmile zkontrolujete a označíte aplikace jako kritické, zobrazí se zde." + }, + "sendReminders": { + "message": "Odeslat připomenutí" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Jakmile označíte aplikace jako kritické, zobrazí se zde." }, + "viewAtRiskMembers": { + "message": "Zobrazit ohrožené členy" + }, + "viewAtRiskApplications": { + "message": "Zobrazit ohrožené aplikace" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ z $TOTAL$ kritických aplikací je ohrožených kvůli ohroženým heslům", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Kritické aplikace ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ ohrožených aplikací", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ ohrožených hesel", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Obeznámení členové ($COUNT$)", "placeholders": { @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "Ohrožení členové" }, - "membersAtRiskActivityDescription": { - "message": "Členové, kteří upravují přístup k rizikovým položkám pro kritické aplikace" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Členové s přístupem k rizikovým položkám pro kritické aplikace" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "$COUNT$ členů v ohrožení", "placeholders": { "count": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Chybné hlavní heslo" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Neplatné hlavní heslo. Potvrďte správnost e-mailu a zda byl Váš účet vytvořen na $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Neplatné heslo souboru, použijte heslo zadané při vytvoření souboru exportu." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Žádné položky k zobrazení." }, + "noItemsInTrash": { + "message": "Žádné položky v koši" + }, + "noItemsInTrashDesc": { + "message": "Položky, které smažete, se zde zobrazí a budou trvale smazány po 30 dnech" + }, + "noItemsInVault": { + "message": "Žádné položky v trezoru" + }, + "emptyVaultDescription": { + "message": "Trezor chrání více než jen Vaše hesla. Bezpečně zde uložte zabezpečená přihlášení, ID, karty a poznámky." + }, + "emptyFavorites": { + "message": "Nemáte oblíbené žádné položky" + }, + "emptyFavoritesDesc": { + "message": "Přidejte často používané položky do oblíbených pro rychlý přístup." + }, + "noSearchResults": { + "message": "Nebyly vráceny žádné výsledky hledání" + }, + "clearFiltersOrTryAnother": { + "message": "Vymažte filtry nebo zkuste jiný hledaný výraz" + }, "noPermissionToViewAllCollectionItems": { "message": "Nemáte oprávnění k zobrazení všech položek v této sbírce." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organizace byla deaktivována" }, + "organizationIsSuspended": { + "message": "Organizace je deaktivována" + }, + "organizationIsSuspendedDesc": { + "message": "K položkám v deaktivované organizaci nemáte přístup. Požádejte o pomoc vlastníka organizace." + }, "secretsAccessSuspended": { "message": "K deaktivované organizaci nemáte přístup. Požádejte o pomoc vlastníka organizace." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Servisní účty nelze vytvořit v pozastavených organizacích. Požádejte o pomoc vlastníka organizace." }, - "disabledOrganizationFilterError": { - "message": "K položkám v deaktivované organizaci nemáte přístup. Požádejte o pomoc vlastníka organizace." - }, "licenseIsExpired": { "message": "Licence vypršela." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifikátor" }, - "ssoIdentifierHintPartOne": { - "message": "Poskytněte toto ID Vašim členům pro přihlášení pomocí SSO. Chcete-li obejít tento krok, nastavte ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Zadejte toto ID svým členům a přihlaste se pomocí SSO. Členové mohou přeskočit zadání tohoto identifikátoru během SSO, pokud je nastavena požadovaná doména. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Dozvědět se více", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Zrušit propojení s SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Bude exportován jen trezor organizace přidružený k $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Bude exportován jen trezor organizace přidružený k $ORGANIZATION$. Položky mých sbírek nebudou zahrnuty.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Přístup byl odepřen. Nemáte oprávnění k zobrazení této stránky." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Neznámý tajný klíč. Možná budete muset požádat o oprávnění pro přístup k tomuto tajnému klíči." }, + "unknownServiceAccount": { + "message": "Neznámý strojový účet. Možná budete muset požádat o oprávnění pro přístup k tomuto strojovému účtu." + }, "unknownProject": { "message": "Neznámý projekt. Možná budete muset požádat o oprávnění pro přístup k tomuto projektu." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Skupina/Uživatel" }, - "lowKdfIterations": { - "message": "Nízke iterace KDF" - }, - "updateLowKdfIterationsDesc": { - "message": "Aktualizujte své nastavení šifrování tak, aby splňovalo nová bezpečnostní doporučení a zlepšilo ochranu účtu." - }, "kdfSettingsChangeLogoutWarning": { "message": "Pokračováním se odhlásíte ze všech aktivních relací. Budete se muset znovu přihlásit a dokončit nastavení dvoufázového přihlášení, pokud nějaké máte. Před změnou nastavení šifrování doporučujeme exportovat trezor, aby nedošlo ke ztrátě dat." }, @@ -8485,7 +8611,7 @@ } } }, - "accessedProjectWithId": { + "accessedProjectWithIdentifier": { "message": "Přístup k projektu s ID: $PROJECT_ID$.", "placeholders": { "project_id": { @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "ID smazaného strojového účtu: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Úprava projektu s ID: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Přidán uživatel $USER_ID$ do strojového účtu s ID $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Smazaný uživatel $USER_ID$ ze strojového účtu s ID $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Odebrána skupina $GROUP_ID$ ze strojového účtu s ID $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Vytvořený strojový účet s ID $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Přidána skupina $GROUP_ID$ do strojového účtu s ID $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Smazán strojový účet s ID $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Smazání projektu s ID: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Přiřadit" }, + "assignTasks": { + "message": "Přiřadit úkoly" + }, "assignToCollections": { "message": "Přiřadit ke sbírkám" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Nepodařilo se uložit integraci. Opakujte akci později." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Pro provedení této akce musíte být vlastníkem organizace." + }, "failedToDeleteIntegration": { "message": "Nepodařilo se smazat integraci. Opakujte akci později." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Nízké iterace KDF. Zvyšte iterace pro zvýšení bezpečnosti Vašeho účtu." - }, - "changeKDFSettings": { - "message": "Změnit nastavení KDF" - }, "secureYourInfrastructure": { "message": "Zabezpečte Vaši infrastrukturu" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Hledat v archivu" }, - "archive": { - "message": "Archivovat" + "archiveNoun": { + "message": "Archiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archivovat", + "description": "Verb" }, "noItemsInArchive": { "message": "Žádné položky v archivu" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Rozšíření Bitwarden nainstalováno!" }, + "openTheBitwardenExtension": { + "message": "Otevřít rozšíření Bitwarden" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "Rozšíření Bitwarden je nainstalováno! Otevřete rozšíření pro přihlášení a začněte používat automatické vyplňování." + }, "openExtensionToAutofill": { "message": "Otevřete rozšíření pro příhlášení a spuštění automatického vyplňování." }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 43fc75230a9..59dd63da969 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index f5441544b96..2ffab8a80c9 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Opret nyt login-emne" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Kritiske apps ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Underrettede medlemmer ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Udsatte medlemmer" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Ugyldig hovedadgangskode" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Ugyldig filadgangskode. Brug samme adgangskode, som da du oprettede eksportfilen." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Der er ingen emner at vise." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "Ingen tilladelse til at se alle emner i denne samling." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisation suspenderet" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspenderede organisationer kan ikke tilgås. Kontakt organisationsejeren for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Tjenestekonti kan ikke oprettes i suspenderede organisationer. Kontakt organisationsejeren for hjælp." }, - "disabledOrganizationFilterError": { - "message": "Emner i deaktiverede organisationer kan ikke tilgås. Kontakt oganisationsejeren for hjælp." - }, "licenseIsExpired": { "message": "Licensen er udløbet." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO-identifikator" }, - "ssoIdentifierHintPartOne": { - "message": "Giv medlemmerne dette ID til indlogning med SSO. For at omgå dette trin, opsæt ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Fjern SSO-tilknytning" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Adgang nægtet. Nødvendig tilladelse til at se siden mangler." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Gruppe/Bruger" }, - "lowKdfIterations": { - "message": "Få KDF-iterationer" - }, - "updateLowKdfIterationsDesc": { - "message": "Opdatér krypteringsindstillingerne for at opfylde nye sikkerhedsanbefalinger og forbedre kontobeskyttelsen." - }, "kdfSettingsChangeLogoutWarning": { "message": "Fortsættes, logges alle aktive sessioner af. Man vil skulle logge ind igen og færdiggøre opsætningen af totrinsindlogning. Eksport af boksen inden krypteringsindstillingerne ændres anbefales for at forhindre datatab." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Tildel" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Tildel til samlinger" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Få KDF-iterationer. Forøg iterationer for at forbedre kontosikkerheden." - }, - "changeKDFSettings": { - "message": "Skift KDF-indstillinger" - }, "secureYourInfrastructure": { "message": "Sikr infrastrukturen" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 012f562fe8c..d45bf25a138 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -3,7 +3,7 @@ "message": "Alle Anwendungen" }, "activity": { - "message": "Activity" + "message": "Aktivität" }, "appLogoLabel": { "message": "Bitwarden-Logo" @@ -59,8 +59,61 @@ "createNewLoginItem": { "message": "Neuen Zugangsdaten-Eintrag erstellen" }, - "criticalApplicationsActivityDescription": { - "message": "Once you mark applications critical, they will display here." + "percentageCompleted": { + "message": "Zu $PERCENT$% abgeschlossen", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ von $TOTAL$ Sicherheitsaufgaben abgeschlossen", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Fortschritt der Passwortänderung" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Mitglieder Aufgaben zuweisen, um den Fortschritt zu überwachen" + }, + "onceYouReviewApplications": { + "message": "Sobald du Anwendungen überprüft und als kritisch markiert hast, werden sie hier angezeigt." + }, + "sendReminders": { + "message": "Erinnerungen senden" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "Sobald du Anwendungen als kritisch markierst, werden sie hier angezeigt." + }, + "viewAtRiskMembers": { + "message": "Gefährdete Mitglieder anzeigen" + }, + "viewAtRiskApplications": { + "message": "Gefährdete Anwendungen anzeigen" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ von $TOTAL$ kritischen Anwendungen sind aufgrund gefährdeter Passwörter gefährdet", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } }, "criticalApplicationsWithCount": { "message": "Kritische Anwendungen ($COUNT$)", @@ -72,7 +125,25 @@ } }, "countOfCriticalApplications": { - "message": "$COUNT$ critical applications", + "message": "$COUNT$ kritische Anwendungen", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ gefährdete Anwendungen", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ Passwörter gefährdet", "placeholders": { "count": { "content": "$1", @@ -102,13 +173,13 @@ "message": "Während Benutzer Zugangsdaten speichern, werden hier Anwendungen angezeigt, die alle gefährdeten Passwörter anzeigen. Markiere kritische Anwendungen und benachrichtige Benutzer, um Passwörter zu ändern." }, "noCriticalApplicationsTitle": { - "message": "You haven’t marked any applications as critical" + "message": "Du hast keine Anwendung als kritisch markiert" }, "noCriticalApplicationsDescription": { - "message": "Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "Wähle deine kritischsten Anwendungen aus, um Sicherheitsmaßnahmen für deine Benutzer zu priorisieren und gefährdete Passwörter zu beheben." }, "markCriticalApplications": { - "message": "Select critical applications" + "message": "Kritische Anwendungen auswählen" }, "markAppAsCritical": { "message": "Anwendung als kritisch markieren" @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Gefährdete Mitglieder" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Mitglieder mit Zugriff auf gefährdete Einträge für kritische Anwendungen" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ gefährdete Mitglieder", "placeholders": { "count": { "content": "$1", @@ -204,10 +275,10 @@ "message": "Anwendungen insgesamt" }, "unmarkAsCritical": { - "message": "Unmark as critical" + "message": "Markierung als kritisch aufheben" }, "criticalApplicationUnmarkedSuccessfully": { - "message": "Successfully unmarked application as critical" + "message": "Markierung der Anwendung als kritisch erfolgreich aufgehoben" }, "whatTypeOfItem": { "message": "Um welche Art von Eintrag handelt es sich hierbei?" @@ -762,79 +833,79 @@ "message": "Eintrag anzeigen" }, "newItemHeaderLogin": { - "message": "New Login", + "message": "Neue Zugangsdaten", "description": "Header for new login item type" }, "newItemHeaderCard": { - "message": "New Card", + "message": "Neue Karte", "description": "Header for new card item type" }, "newItemHeaderIdentity": { - "message": "New Identity", + "message": "Neue Identität", "description": "Header for new identity item type" }, "newItemHeaderNote": { - "message": "New Note", + "message": "Neue Notiz", "description": "Header for new note item type" }, "newItemHeaderSshKey": { - "message": "New SSH key", + "message": "Neuer SSH-Schlüssel", "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "New Text Send", + "message": "Neues Text-Send", "description": "Header for new text send" }, "newItemHeaderFileSend": { - "message": "New File Send", + "message": "Neues Datei-Send", "description": "Header for new file send" }, "editItemHeaderLogin": { - "message": "Edit Login", + "message": "Zugangsdaten bearbeiten", "description": "Header for edit login item type" }, "editItemHeaderCard": { - "message": "Edit Card", + "message": "Karte bearbeiten", "description": "Header for edit card item type" }, "editItemHeaderIdentity": { - "message": "Edit Identity", + "message": "Identität bearbeiten", "description": "Header for edit identity item type" }, "editItemHeaderNote": { - "message": "Edit Note", + "message": "Notiz bearbeiten", "description": "Header for edit note item type" }, "editItemHeaderSshKey": { - "message": "Edit SSH key", + "message": "SSH-Schlüssel bearbeiten", "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Edit Text Send", + "message": "Text-Send bearbeiten", "description": "Header for edit text send" }, "editItemHeaderFileSend": { - "message": "Edit File Send", + "message": "Datei-Send bearbeiten", "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "Zugangsdaten anzeigen", "description": "Header for view login item type" }, "viewItemHeaderCard": { - "message": "View Card", + "message": "Karte anzeigen", "description": "Header for view card item type" }, "viewItemHeaderIdentity": { - "message": "View Identity", + "message": "Identität anzeigen", "description": "Header for view identity item type" }, "viewItemHeaderNote": { - "message": "View Note", + "message": "Notiz anzeigen", "description": "Header for view note item type" }, "viewItemHeaderSshKey": { - "message": "View SSH key", + "message": "SSH-Schlüssel anzeigen", "description": "Header for view SSH key item type" }, "new": { @@ -944,13 +1015,13 @@ "message": "Lizenznummer kopieren" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Privaten Schlüssel kopieren" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Öffentlichen Schlüssel kopieren" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Fingerabdruck kopieren" }, "copyName": { "message": "Name kopieren" @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Ungültiges Master-Passwort" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ungültiges Master-Passwort. Überprüfe, ob deine E-Mail-Adresse korrekt ist und dein Konto auf $HOST$ erstellt wurde.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Ungültiges Dateipasswort. Bitte verwende das Passwort, das du beim Erstellen der Exportdatei eingegeben hast." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Keine Einträge vorhanden." }, + "noItemsInTrash": { + "message": "Keine Einträge im Papierkorb" + }, + "noItemsInTrashDesc": { + "message": "Einträge, die du löschst, erscheinen hier und werden nach 30 Tagen dauerhaft gelöscht" + }, + "noItemsInVault": { + "message": "Keine Einträge im Tresor" + }, + "emptyVaultDescription": { + "message": "Der Tresor schützt mehr als nur deine Passwörter. Speicher hier sichere Zugangsdaten, Identitäten, Karten und Notizen." + }, + "emptyFavorites": { + "message": "Du hast keine Einträge zu Favoriten hinzugefügt" + }, + "emptyFavoritesDesc": { + "message": "Füge häufig verwendete Einträge zu Favoriten hinzu, um schnell darauf zuzugreifen." + }, + "noSearchResults": { + "message": "Keine Suchergebnisse gefunden" + }, + "clearFiltersOrTryAnother": { + "message": "Filter löschen oder es mit einem anderen Suchbegriff versuchen" + }, "noPermissionToViewAllCollectionItems": { "message": "Du hast nicht die Berechtigung, alle Einträge in dieser Sammlung anzuzeigen." }, @@ -1590,7 +1694,7 @@ "message": "Wiederherstellungscode" }, "invalidRecoveryCode": { - "message": "Invalid recovery code" + "message": "Ungültiger Wiederherstellungscode" }, "authenticatorAppTitle": { "message": "Authenticator App" @@ -2136,10 +2240,10 @@ "message": "Sammlung auswählen" }, "importTargetHintCollection": { - "message": "Select this option if you want the imported file contents moved to a collection" + "message": "Wähle diese Option, wenn die Inhalte der Import-Datei in eine Sammlung verschoben werden sollen" }, "importTargetHintFolder": { - "message": "Select this option if you want the imported file contents moved to a folder" + "message": "Wähle diese Option, wenn die Inhalte der Import-Datei in einen Ordner verschoben werden sollen" }, "importUnassignedItemsError": { "message": "Die Datei enthält nicht zugewiesene Einträge." @@ -2188,7 +2292,7 @@ "message": "Die Sprache des Web-Tresors ändern." }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Website-Symbole anzeigen und URLs zum Ändern von Passwörtern abrufen" }, "default": { "message": "Standard" @@ -2900,10 +3004,10 @@ } }, "showPricingSummary": { - "message": "Show pricing summary" + "message": "Preisübersicht anzeigen" }, "hidePricingSummary": { - "message": "Hide pricing summary" + "message": "Preisübersicht ausblenden" }, "summary": { "message": "Zusammenfassung" @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisation ist deaktiviert" }, + "organizationIsSuspended": { + "message": "Organisation ist deaktiviert" + }, + "organizationIsSuspendedDesc": { + "message": "Auf Einträge in deaktivierten Organisationen kann nicht zugegriffen werden. Kontaktiere den Besitzer deiner Organisation, um Unterstützung zu erhalten." + }, "secretsAccessSuspended": { "message": "Auf deaktivierte Organisationen kann nicht zugegriffen werden. Bitte wende dich an deinen Organisationseigentümer." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Dienstkonten können in deaktivierten Organisationen nicht erstellt werden. Bitte kontaktiere deinen Organisationseigentümer, um Unterstützung zu erhalten." }, - "disabledOrganizationFilterError": { - "message": "Auf Einträge in deaktivierten Organisationen kann nicht zugegriffen werden. Kontaktiere deinen Organisationseigentümer für Unterstützung." - }, "licenseIsExpired": { "message": "Lizenz ist abgelaufen." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO-Kennung" }, - "ssoIdentifierHintPartOne": { - "message": "Gib diese ID deinen Mitgliedern zur Anmeldung über SSO. Um diesen Schritt zu umgehen, aktiviere die ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Gib deinen Mitgliedern diese ID für die Anmeldung mit SSO. Mitglieder können die Eingabe dieser Kennung während des SSO überspringen, wenn eine beanspruchte Domain eingerichtet ist. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Mehr erfahren", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "SSO-Verknüpfung aufheben" @@ -5558,10 +5669,10 @@ "message": "Aufgrund einer Unternehmensrichtlinie darfst du keine Einträge in deinem persönlichen Tresor speichern. Ändere die Eigentümer-Option in eine Organisation und wähle aus den verfügbaren Sammlungen." }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "Desktop-Autotype-Standard­einstellung" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "Aktiviere Desktop-Autotype standardmäßig für Mitglieder. Mitglieder können Autotype im Desktop-Client manuell deaktivieren.", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { @@ -6729,7 +6840,7 @@ "message": "SSO deaktiviert" }, "emailMustLoginWithSso": { - "message": "$EMAIL$ must login with Single Sign-on", + "message": "$EMAIL$ muss sich mit Single Sign-On anmelden", "placeholders": { "email": { "content": "$1", @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Nur der mit $ORGANIZATION$ verknüpfte Organisations-Tresor wird exportiert.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Nur der mit $ORGANIZATION$ verknüpfte Organisations-Tresor wird exportiert. Meine Eintrags-Sammlungen werden nicht eingeschlossen.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Zugriff verweigert. Du hast keine Berechtigung, diese Seite zu sehen." }, @@ -7127,10 +7256,13 @@ "message": "Unbekannter Eintrag. Du musst möglicherweise eine Berechtigung anfordern, um auf diesen Eintrag zuzugreifen." }, "unknownSecret": { - "message": "Unknown secret, you may need to request permission to access this secret." + "message": "Unbekanntes Geheimnis, möglicherweise müssen Sie eine Berechtigung anfordern, um auf dieses Geheimnis zuzugreifen." + }, + "unknownServiceAccount": { + "message": "Unbekanntes Gerätekonto. Du musst möglicherweise eine Berechtigung anfordern, um auf dieses Gerätekonto zuzugreifen." }, "unknownProject": { - "message": "Unknown project, you may need to request permission to access this project." + "message": "Unbekanntes Projekt. Du musst möglicherweise eine Berechtigung anfordern, um auf dieses Projekt zuzugreifen." }, "cannotSponsorSelf": { "message": "Du kannst nicht für das aktive Konto einlösen. Gib eine andere E-Mail ein." @@ -7520,7 +7652,7 @@ "message": "Aus" }, "connected": { - "message": "Connected" + "message": "Verbunden" }, "members": { "message": "Mitglieder" @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Gruppe/Benutzer" }, - "lowKdfIterations": { - "message": "Geringe KDF-Iterationen" - }, - "updateLowKdfIterationsDesc": { - "message": "Aktualisiere deine Verschlüsselungseinstellungen, um neue Sicherheitsempfehlungen zu erfüllen und den Kontoschutz zu verbessern." - }, "kdfSettingsChangeLogoutWarning": { "message": "Wenn du fortfährst, wirst du von allen aktiven Sitzungen abgemeldet. Du musst dich erneut anmelden und, falls eingerichtet, die Zwei-Faktor-Authentifizierung durchführen. Wir empfehlen, deinen Tresor zu exportieren, bevor du deine Verschlüsselungseinstellungen änderst, um Datenverluste zu vermeiden." }, @@ -8459,7 +8585,7 @@ } }, "permanentlyDeletedSecretWithId": { - "message": "Permanently deleted a secret with identifier: $SECRET_ID$", + "message": "Ein Geheimnis mit der Kennung $SECRET_ID$ wurde dauerhaft gelöscht", "placeholders": { "secret_id": { "content": "$1", @@ -8468,7 +8594,7 @@ } }, "restoredSecretWithId": { - "message": "Restored a secret with identifier: $SECRET_ID$", + "message": "Ein Geheimnis mit der Kennung $SECRET_ID$ wurde wiederhergestellt", "placeholders": { "secret_id": { "content": "$1", @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Auf ein Projekt zugegriffen mit der Kennung: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8495,7 +8621,7 @@ } }, "nameUnavailableProjectDeleted": { - "message": "Deleted project Id: $PROJECT_ID$", + "message": "Projekt mit der ID $PROJECT_ID$ gelöscht", "placeholders": { "project_id": { "content": "$1", @@ -8504,7 +8630,7 @@ } }, "nameUnavailableSecretDeleted": { - "message": "Deleted secret Id: $SECRET_ID$", + "message": "Geheimnis mit der ID $SECRET_ID$ gelöscht", "placeholders": { "secret_id": { "content": "$1", @@ -8512,8 +8638,17 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Gelöschtes Gerätekonto mit der Kennung: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { - "message": "Edited a project with identifier: $PROJECT_ID$", + "message": "Ein Projekt mit der Kennung $PROJECT_ID$ bearbeitet", "placeholders": { "project_id": { "content": "$1", @@ -8521,8 +8656,78 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Benutzer: $USER_ID$ hinzugefügt zu Gerätekonto mit der Kennung: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Benutzer: $USER_ID$ entfernt aus dem Gerätekonto mit der Kennung: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Gruppe: $GROUP_ID$ entfernt aus dem Gerätekonto mit der Kennung: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Gerätekonto erstellt mit der Kennung: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Gruppe: $GROUP_ID$ hinzugefügt zu Gerätekonto mit der Kennung: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Gelöschtes Gerätekonto mit der Kennung: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { - "message": "Deleted a project with identifier: $PROJECT_ID$", + "message": "Ein Projekt mit der Kennung $PROJECT_ID$ gelöscht", "placeholders": { "project_id": { "content": "$1", @@ -8531,7 +8736,7 @@ } }, "createdProjectWithId": { - "message": "Created a new project with identifier: $PROJECT_ID$", + "message": "Ein neues Projekt mit der Kennung $PROJECT_ID$ erstellt", "placeholders": { "project_id": { "content": "$1", @@ -9028,23 +9233,23 @@ "message": "Verwaltung der Sammlungen" }, "collectionManagementDescription": { - "message": "Configure the collection behavior for the organization" + "message": "Konfiguriere das Sammlungs-Verhalten für die Organisation" }, "allowAdminAccessToAllCollectionItemsDescription": { - "message": "Allow owners and admins to manage all collections and items from the Admin Console" + "message": "Erlaube Besitzern und Administratoren, alle Sammlungen und Einträge über die Admin-Konsole zu verwalten" }, "restrictCollectionCreationDescription": { - "message": "Restrict collection creation to owners and admins" + "message": "Beschränke die Erstellung von Sammlungen auf Besitzer und Administratoren" }, "restrictCollectionDeletionDescription": { - "message": "Restrict collection deletion to owners and admins" + "message": "Beschränke das Löschen von Sammlungen auf Besitzer und Administratoren" }, "restrictItemDeletionDescriptionStart": { - "message": "Restrict item deletion to members with the ", + "message": "Beschränke das Löschen von Einträgen auf Mitglieder mit der ", "description": "This will be used as part of a larger sentence, broken up to allow styling of the middle portion. Full sentence: 'Restrict item deletion to members with the [Manage collection] permission'" }, "restrictItemDeletionDescriptionEnd": { - "message": " permission", + "message": " Berechtigung", "description": "This will be used as part of a larger sentence, broken up to allow styling of the middle portion. Full sentence: 'Restrict item deletion to members with the [Manage collection] permission'" }, "updatedCollectionManagement": { @@ -9124,7 +9329,7 @@ } }, "limitCollectionCreationEnabled": { - "message": "Turned on Restrict collection creation setting $ID$.", + "message": "Die Einstellung Sammlungs-Erstellung einschränken wurde für $ID$ aktiviert.", "placeholders": { "id": { "content": "$1", @@ -9133,7 +9338,7 @@ } }, "limitCollectionCreationDisabled": { - "message": "Turned off Restrict collection creation setting $ID$.", + "message": "Die Einstellung Sammlungs-Erstellung einschränken wurde für $ID$ deaktiviert.", "placeholders": { "id": { "content": "$1", @@ -9142,7 +9347,7 @@ } }, "limitCollectionDeletionEnabled": { - "message": "Turned on Restrict collection deletion setting $ID$.", + "message": "Die Einstellung Sammlungs-Löschung einschränken wurde für $ID$ aktiviert.", "placeholders": { "id": { "content": "$1", @@ -9151,7 +9356,7 @@ } }, "limitCollectionDeletionDisabled": { - "message": "Turned off Restrict collection deletion setting $ID$.", + "message": "Die Einstellung Sammlungs-Löschung einschränken wurde für $ID$ deaktiviert.", "placeholders": { "id": { "content": "$1", @@ -9160,7 +9365,7 @@ } }, "limitItemDeletionEnabled": { - "message": "Turned on Restrict item deletion setting $ID$.", + "message": "Die Einstellung Eintrags-Löschung einschränken wurde für $ID$ aktiviert.", "placeholders": { "id": { "content": "$1", @@ -9169,7 +9374,7 @@ } }, "limitItemDeletionDisabled": { - "message": "Turned off Restrict item deletion setting $ID$.", + "message": "Die Einstellung Eintrags-Löschung einschränken wurde für $ID$ deaktiviert.", "placeholders": { "id": { "content": "$1", @@ -9178,7 +9383,7 @@ } }, "allowAdminAccessToAllCollectionItemsEnabled": { - "message": "Turned on Allow owners and admins to manage all collections and items setting $ID$.", + "message": "Die Einstellung Besitzern und Administratoren erlauben, alle Sammlungen und Einträge zu verwalten wurde für $ID$ aktiviert.", "placeholders": { "id": { "content": "$1", @@ -9187,7 +9392,7 @@ } }, "allowAdminAccessToAllCollectionItemsDisabled": { - "message": "Turned off Allow owners and admins to manage all collections and items setting $ID$.", + "message": "Die Einstellung Besitzern und Administratoren erlauben, alle Sammlungen und Einträge zu verwalten wurde für $ID$ deaktiviert.", "placeholders": { "id": { "content": "$1", @@ -9357,6 +9562,9 @@ "assign": { "message": "Zuweisen" }, + "assignTasks": { + "message": "Aufgaben zuweisen" + }, "assignToCollections": { "message": "Sammlungen zuweisen" }, @@ -9755,8 +9963,11 @@ "failedToSaveIntegration": { "message": "Fehler beim Speichern der Integration. Bitte versuche es später erneut." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Du musst der Besitzer der Organisation sein, um diese Aktion auszuführen." + }, "failedToDeleteIntegration": { - "message": "Failed to delete integration. Please try again later." + "message": "Löschen der Integration fehlgeschlagen. Bitte versuche es später erneut." }, "deviceIdMissing": { "message": "Geräte-ID fehlt" @@ -9783,7 +9994,7 @@ } }, "updateIntegrationButtonDesc": { - "message": "Update $INTEGRATION$", + "message": "$INTEGRATION$ aktualisieren", "placeholders": { "integration": { "content": "$1", @@ -9855,7 +10066,7 @@ "message": "Bearer-Token" }, "repositoryNameHint": { - "message": "Name of the repository to ingest into" + "message": "Name des Codespeichers, in den importiert werden soll" }, "index": { "message": "Index" @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Geringe KDF-Iterationen. Erhöhe deine Iterationen, um die Sicherheit deines Kontos zu steigern." - }, - "changeKDFSettings": { - "message": "KDF-Einstellungen ändern" - }, "secureYourInfrastructure": { "message": "Sichere deine Infrastruktur" }, @@ -10995,10 +11200,10 @@ "message": "Gefährdetes Passwort ändern" }, "changeAtRiskPasswordAndAddWebsite": { - "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." + "message": "Diese Zugangsdaten sind gefährdet und es fehlt eine Website. Füge eine Website hinzu und ändere das Passwort für mehr Sicherheit." }, "missingWebsite": { - "message": "Missing website" + "message": "Fehlende Webseite" }, "removeUnlockWithPinPolicyTitle": { "message": "Entsperren mit PIN entfernen" @@ -11016,22 +11221,27 @@ "message": "Diese Ereignisse sind nur Beispiele und spiegeln keine realen Ereignisse in deiner Bitwarden-Organisation wider." }, "viewEvents": { - "message": "View Events" + "message": "Ereignisse anzeigen" }, "cannotCreateCollection": { "message": "Kostenlose Organisationen können bis zu 2 Sammlungen haben. Upgrade auf ein kostenpflichtiges Abo, um mehr Sammlungen hinzuzufügen." }, "searchArchive": { - "message": "Search archive" + "message": "Archiv durchsuchen" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archivieren", + "description": "Verb" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Keine Einträge im Archiv" }, "archivedItemsDescription": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen sowie Auto-Ausfüllen-Vorschlägen ausgeschlossen." }, "businessUnit": { "message": "Geschäftsbereich" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden-Erweiterung installiert!" }, + "openTheBitwardenExtension": { + "message": "Die Bitwarden-Erweiterung öffnen" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "Die Bitwarden-Erweiterung ist installiert! Öffne die Erweiterung, um dich anzumelden und das automatische Ausfüllen zu starten." + }, "openExtensionToAutofill": { "message": "Öffne die Erweiterung, um dich anzumelden und Auto-Ausfüllen zu nutzen." }, @@ -11188,10 +11404,10 @@ "description": "Error message shown when trying to add credit to a trialing organization without a billing address." }, "aboutThisSetting": { - "message": "About this setting" + "message": "Über diese Einstellung" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden verwendet gespeicherte Zugangsdaten-URIs, um zu bestimmen, welches Symbol oder welche Passwort-Ändern-URL verwendet werden soll, um dein Erlebnis zu verbessern. Es werden keine Informationen erfasst oder gespeichert, wenn du diesen Dienst nutzt." }, "billingAddress": { "message": "Rechnungsadresse" @@ -11311,7 +11527,7 @@ "message": "Maßnahme erforderlich: In den Zahlungsdetails fehlt eine Steueridentifikationsnummer. Wenn keine Steuernummer hinzugefügt wird, können deine Rechnungen zusätzliche Steuern enthalten." }, "moreBreadcrumbs": { - "message": "More breadcrumbs", + "message": "Weitere Navigationspfade", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." }, "addTaxId": { @@ -11355,46 +11571,46 @@ } }, "confirmKeyConnectorDomain": { - "message": "Confirm Key Connector domain" + "message": "Key Connector-Domain bestätigen" }, "requiredToVerifyBankAccountWithStripe": { - "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + "message": "Zahlungen mit einem Bankkonto sind nur für Kunden in den Vereinigten Staaten verfügbar. Du musst dein Bankkonto verifizieren. Wir werden innerhalb der nächsten 1–2 Werktage eine geringfügige Einzahlung vornehmen. Wenn das Bankkonto nicht verifiziert wird, führt dies zu einer verpassten Zahlung und dein Abonnement wird ausgesetzt." }, "verifyBankAccountWithStripe": { - "message": "We have made a micro-deposit to your bank account. This may take 1-2 business days. When you see the deposit in your account, you can verify your bank account. Failure to verify your bank account will result in a missed payment and your subscription will be suspended." + "message": "Wir haben eine geringfügige Einzahlung auf dein Bankkonto vorgenommen. Dies kann 1–2 Werktage dauern. Sobald du die Einzahlung auf deinem Konto siehst, kannst du dein Bankkonto verifizieren. Wenn das Bankkonto nicht verifiziert wird, führt dies zu einer verpassten Zahlung und dein Abonnement wird ausgesetzt." }, "verifyNow": { - "message": "Verify now." + "message": "Jetzt verifizieren." }, "additionalStorageGB": { - "message": "Additional storage GB" + "message": "Zusätzlicher Speicher GB" }, "additionalServiceAccountsV2": { - "message": "Additional machine accounts" + "message": "Zusätzliche Gerätekonten" }, "secretsManagerSeats": { - "message": "Secrets Manager seats" + "message": "Secrets-Manager-Plätze" }, "additionalStorage": { - "message": "Additional Storage" + "message": "Zusätzlicher Speicher" }, "expandPurchaseDetails": { - "message": "Expand purchase details" + "message": "Kaufdetails erweitern" }, "collapsePurchaseDetails": { - "message": "Collapse purchase details" + "message": "Kaufdetails einklappen" }, "familiesMembership": { - "message": "Families membership" + "message": "Families-Mitgliedschaft" }, "planDescPremium": { - "message": "Complete online security" + "message": "Umfassende Online-Sicherheit" }, "planDescFamiliesV2": { - "message": "Premium security for your family" + "message": "Premium-Sicherheit für deine Familie" }, "planDescFreeV2": { - "message": "Share with $COUNT$ other user", + "message": "Mit $COUNT$ anderen Benutzer teilen", "placeholders": { "count": { "content": "$1", @@ -11403,37 +11619,37 @@ } }, "planDescEnterpriseV2": { - "message": "Advanced capabilities for any organization" + "message": "Erweiterte Funktionen für jede Organisation" }, "planNameCustom": { - "message": "Custom plan" + "message": "Individueller Plan" }, "planDescCustom": { - "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + "message": "Bitwarden wächst mit Unternehmen jeder Größe mit, um Passwörter und vertrauliche Informationen zu sichern. Wenn du Teil eines großen Unternehmens bist, kontaktiere den Vertrieb, um ein Angebot anzufordern." }, "builtInAuthenticator": { - "message": "Built-in authenticator" + "message": "Integrierter Authenticator" }, "breachMonitoring": { - "message": "Breach monitoring" + "message": "Datenpannen-Überwachung" }, "andMoreFeatures": { - "message": "And more!" + "message": "Und mehr!" }, "secureFileStorage": { - "message": "Secure file storage" + "message": "Sicherer Dateispeicher" }, "familiesUnlimitedSharing": { - "message": "Unlimited sharing - choose who sees what" + "message": "Unbegrenztes Teilen – wähle, wer was sieht" }, "familiesUnlimitedCollections": { - "message": "Unlimited family collections" + "message": "Unbegrenzte Familiensammlungen" }, "familiesSharedStorage": { - "message": "Shared storage for important family info" + "message": "Gemeinsamer Speicher für wichtige Familieninformationen" }, "limitedUsersV2": { - "message": "Up to $COUNT$ members", + "message": "Bis zu $COUNT$ Mitglieder", "placeholders": { "count": { "content": "$1", @@ -11442,7 +11658,7 @@ } }, "limitedCollectionsV2": { - "message": "Up to $COUNT$ collections", + "message": "Bis zu $COUNT$ Sammlungen", "placeholders": { "count": { "content": "$1", @@ -11451,13 +11667,13 @@ } }, "alwaysFree": { - "message": "Always free" + "message": "Immer kostenlos" }, "twoSecretsIncluded": { - "message": "2 secrets" + "message": "2 Geheimnisse" }, "projectsIncludedV2": { - "message": "$COUNT$ project(s)", + "message": "$COUNT$ Projekt(e)", "placeholders": { "count": { "content": "$1", @@ -11466,13 +11682,13 @@ } }, "secureItemSharing": { - "message": "Secure item sharing" + "message": "Sicheres Teilen von Einträgen" }, "scimSupport": { - "message": "SCIM support" + "message": "SCIM-Unterstützung" }, "includedMachineAccountsV2": { - "message": "$COUNT$ machine accounts", + "message": "$COUNT$ Gerätekonten", "placeholders": { "count": { "content": "$1", @@ -11481,21 +11697,21 @@ } }, "enterpriseSecurityPolicies": { - "message": "Enterprise security policies" + "message": "Sicherheitsrichtlinien für Unternehmen" }, "selfHostOption": { - "message": "Self-host option" + "message": "Selbsthosting-Option" }, "complimentaryFamiliesPlan": { - "message": "Complimentary families plan for all users" + "message": "Kostenloser Families-Plan für alle Benutzer" }, "strengthenCybersecurity": { - "message": "Strengthen cybersecurity" + "message": "Cybersicherheit stärken" }, "boostProductivity": { - "message": "Boost productivity" + "message": "Produktivität steigern" }, "seamlessIntegration": { - "message": "Seamless integration" + "message": "Nahtlose Integration" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 0c876ddd282..436f2176efb 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Κρίσιμες εφαρμογές ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Ειδοποιημένα μέλη ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Μη έγκυρος κύριος κωδικός πρόσβασης" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Μη έγκυρος κωδικός πρόσβασης αρχείου, χρησιμοποιήστε τον κωδικό πρόσβασης που καταχωρήσατε όταν δημιουργήσατε το αρχείο εξαγωγής." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Δεν υπάρχουν στοιχεία στη λίστα." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "Δεν έχετε άδεια να δείτε όλα τα στοιχεία σε αυτήν τη συλλογή." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Ο οργανισμός είναι απενεργοποιημένος." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "Η άδεια χρήσης έληξε." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Αναγνωριστικό SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Δώστε αυτό το αναγνωριστικό στα μέλη σας για να συνδεθείτε με SSO. Για να παρακάμψετε αυτό το βήμα, ρυθμίστε την ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Αποσύνδεση SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Ομάδα/Χρήστης" }, - "lowKdfIterations": { - "message": "Λίγες επαναλήψεις KDF" - }, - "updateLowKdfIterationsDesc": { - "message": "Ενημερώστε τις ρυθμίσεις κρυπτογράφησής σας για να ανταποκριθείτε στις νέες συστάσεις ασφαλείας και να βελτιώσετε την προστασία του λογαριασμού." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Ανάθεση" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Ανάθεση σε συλλογές" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Λίγες επαναλήψεις KDF. Αυξήστε τις επαναλήψεις σας για να βελτιώσετε την ασφάλεια του λογαριασμού σας." - }, - "changeKDFSettings": { - "message": "Αλλαγή ρυθμίσεων KDF" - }, "secureYourInfrastructure": { "message": "Ασφαλίστε την υποδομή σας" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 1b374c97478..c104e9776c1 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -35,6 +35,9 @@ } } }, + "noReportRan": { + "message": "You have not created a report yet" + }, "notifiedMembers": { "message": "Notified members" }, @@ -59,7 +62,41 @@ "createNewLoginItem": { "message": "Create new login item" }, - "onceYouMarkCriticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, "viewAtRiskMembers": { @@ -108,6 +145,15 @@ } } }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -144,6 +190,9 @@ "applicationsMarkedAsCriticalSuccess": { "message": "Applications marked as critical" }, + "applicationsMarkedAsCriticalFail": { + "message": "Failed to mark applications as critical" + }, "application": { "message": "Application" }, @@ -162,11 +211,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription":{ - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -231,6 +280,24 @@ "totalApplications": { "message": "Total applications" }, + "applicationsNeedingReview": { + "message": "Applications needing review" + }, + "newApplicationsWithCount": { + "message": "$COUNT$ new applications", + "placeholders": { + "count": { + "content": "$1", + "example": "13" + } + } + }, + "newApplicationsDescription": { + "message": "Review new applications to mark as critical and keep your organization secure" + }, + "reviewNow": { + "message": "Review now" + }, "unmarkAsCritical": { "message": "Unmark as critical" }, @@ -1205,6 +1272,9 @@ "readingPasskeyLoadingInfo": { "message": "Keep this window open and follow prompts from your browser." }, + "passkeyAuthenticationFailed": { + "message": "Passkey authentication failed. Please try again." + }, "useADifferentLogInMethod": { "message": "Use a different log in method" }, @@ -1499,6 +1569,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1508,6 +1587,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4238,6 +4341,9 @@ "generatingYourRiskInsights": { "message": "Generating your Risk Insights..." }, + "riskInsightsRunReport": { + "message": "Run report" + }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." }, @@ -4804,6 +4910,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4816,9 +4928,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5192,9 +5301,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -7007,6 +7120,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7161,6 +7292,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8408,12 +8542,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8517,8 +8645,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8543,6 +8671,15 @@ "example": "4d34e8a8" } } + }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", @@ -8552,6 +8689,76 @@ "example": "4d34e8a8" } } + }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", @@ -9389,6 +9596,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9784,6 +9994,9 @@ "crowdstrikeEventIntegrationDesc": { "message": "Send event data to your Logscale instance" }, + "datadogEventIntegrationDesc": { + "message": "Send vault event data to your Datadog instance" + }, "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, @@ -10031,12 +10244,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11059,8 +11266,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11175,6 +11387,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index edf4b59c5bd..e5e5d8f972c 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in the bin" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favourited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favourites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisation suspended" }, + "organizationIsSuspended": { + "message": "Organisation is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organisations cannot be accessed. Contact your organisation owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organisations cannot be accessed. Please contact your organisation owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organisations. Please contact your organisation owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organisations cannot be accessed. Contact your organisation owner for assistance." - }, "licenseIsExpired": { "message": "Licence is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organisation owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index ed092371092..c00b02f1e6f 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in the bin" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favourited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favourites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisation is disabled." }, + "organizationIsSuspended": { + "message": "Organisation is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organisations cannot be accessed. Contact your organisation owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organisations cannot be accessed. Please contact your organisation owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organisations. Please contact your organisation owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organisations cannot be accessed. Contact your organisation owner for assistance." - }, "licenseIsExpired": { "message": "Licence is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organisation owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 39ee969de3b..4634635f277 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Krei novan salutan eron" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Sciigitaj membroj ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Nevalida majstra pasvorto" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Nevalida pasvorto de la dosiero, bonvolu uzi la pasvorton, kiun vi enmetis kiam vi kreis la elportan dosieron." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Estas neniu ero por listi" }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "La organizo suspendiĝis" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "Licenco eksvalidiĝis." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Malkonekti SSO-n" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Grupo/Uzanto" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Ŝanĝi agordojn pri KDF" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 7d76870ddc6..85305249a91 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Crear nuevo elemento de inicio de sesión" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Aplicaciones críticas ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Miembros notificados ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Contraseña maestra no válida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Contraseña de archivo no válida, por favor utilice la contraseña que introdujo cuando creó el archivo de exportación." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "No hay elementos que listar." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "No tiene los permisos para ver todos los elementos de esta colección." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "La organización está desactivada." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "No se puede acceder a las organizaciones suspendidas. Póngase en contacto con el propietario de su organización para obtener ayuda." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "No se pueden crear cuentas de servicio en organizaciones suspendidas. Póngase en contacto con el propietario de su organización para obtener ayuda." }, - "disabledOrganizationFilterError": { - "message": "No se puede acceder a los elementos que pertenecen a organizaciones que estén deshabilitadas. Por favor, póngase en contacto con el propietario de la organización para obtener ayuda." - }, "licenseIsExpired": { "message": "Licencia expirada." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Identificador SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Proporcione este ID a sus miembros para iniciar sesión con SSO. Para evitar este paso, configure ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Desenlazar SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Acceso denegado. No tiene permiso para ver esta página." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Grupo/Usuario" }, - "lowKdfIterations": { - "message": "Iteraciones KDF bajas" - }, - "updateLowKdfIterationsDesc": { - "message": "Actualice sus ajustes de cifrado para cumplir con las nuevas recomendaciones de seguridad y mejorar la protección de la cuenta." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Asignar" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Asignar a colecciones" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 83ff0028e0d..23c1a1312eb 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Loo uus kirje" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Teavitatud liikmed ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Vale ülemparool" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Vale parool, palun kasuta seda parooli mille sisestasid eksport faili loomisel." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Puuduvad kirjed, mida kuvada." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "Sul ei ole õigust vaadata kõiki asju selles kogus." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisatsioon on määramata ajaks peatatud" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Peatatud organisatsioonidele ei ole võimalik ligi pääseda. Palun kontakteeruge oma organisatsiooni haldajaga." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Peatatud organisatsioonides ei ole võimalik luua uusi kontosid. Palun kontakteeruge oma organisatsiooni haldajaga." }, - "disabledOrganizationFilterError": { - "message": "Organisatsiooni alla kuuluvatele kirjetele ei ole ligipääsu. Kontakteeru oma organisatsiooni omanikuga." - }, "licenseIsExpired": { "message": "Litsents on aegunud." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Ühenda SSO lahti" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Ligipääs keelatud. Sul pole lubatud seda lehekülge vaadata." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 0dc74e0bb94..8e86873841d 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Master pasahitz baliogabea" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Fitxategi pasahitzak ez du balio; mesedez, erabili esportazio fitxategia sortzean sartutako pasahitza." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Ez dago erakusteko elementurik." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Erakundea desgaituta dago." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Ezin da sartu desgaitutako erakundeetako elementuetara. Laguntza lortzeko, jarri harremanetan zure erakundearekin." - }, "licenseIsExpired": { "message": "Lizentzia iraungi da." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifikatzailea" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "SSO deskonektatu" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Sarbidea ukatuta. Ez duzu baimenik orri hau ikusteko." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 0211b3a7d12..10b51babfc1 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "ایجاد مورد ورود جدید" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "برنامه‌های حیاتی ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "اعضا مطلع شدند ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "اعضای در معرض خطر" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "کلمه عبور اصلی نامعتبر است" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "کلمه عبور پرونده نامعتبر است، لطفاً از کلمه عبوری که هنگام ایجاد پرونده‌ی برون ریزی وارد کردید استفاده کنید." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "هیچ موردی برای نمایش وجود ندارد." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "شما اجازه مشاهده همه موارد در این مجموعه را ندارید." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "سازمان از کار افتاده است" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "سازمان‌های تعلیق شده قابل دسترسی نیستند. لطفاً برای کمک با مالک سازمان خود تماس بگیرید." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "حساب‌های خدماتی را نمی‌توان در سازمان‌های معلق ایجاد کرد. لطفاً برای کمک با مالک سازمان خود تماس بگیرید." }, - "disabledOrganizationFilterError": { - "message": "موارد موجود در سازمان‌های غیرفعال، قابل دسترسی نیستند. برای دریافت کمک با مالک سازمان خود تماس بگیرید." - }, "licenseIsExpired": { "message": "مجوز منقضی شده است." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "شناسه SSO" }, - "ssoIdentifierHintPartOne": { - "message": "این شناسه را در اختیار اعضای خود قرار دهید تا با SSO وارد شوند. برای دور زدن این مرحله، راه‌اندازی کنید ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "لغو پیوند SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "دسترسی رد شد. شما اجازه مشاهده این صفحه را ندارید." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "گروه/کاربر" }, - "lowKdfIterations": { - "message": "تکرار KDF کم" - }, - "updateLowKdfIterationsDesc": { - "message": "تنظیمات رمزگذاری خود را برای رعایت توصیه‌های امنیتی جدید و بهبود حفاظت از حساب به‌روزرسانی کنید." - }, "kdfSettingsChangeLogoutWarning": { "message": "ادامه دادن باعث خروج شما از تمام نشست‌های فعال خواهد شد. برای ادامه باید دوباره وارد شوید و در صورت فعال بودن، ورود دو مرحله‌ای را کامل کنید. توصیه می‌کنیم قبل از تغییر تنظیمات رمزنگاری، از گاوصندوق خود خروجی بگیرید تا از دست رفتن داده‌ها جلوگیری شود." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "اختصاص بدهید" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "اختصاص به مجموعه‌ها" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "تعداد تکرارهای KDF پایین است. برای افزایش امنیت حساب کاربری خود، تعداد تکرارها را افزایش دهید." - }, - "changeKDFSettings": { - "message": "تغییر تنظیمات KDF" - }, "secureYourInfrastructure": { "message": "زیرساخت خود را ایمن کنید" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "افزونه Bitwarden نصب شد!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "افزونه را باز کنید تا وارد شوید و پر کردن خودکار را آغاز کنید." }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 98f37b43cf4..54662ea2274 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Luo uusi kirjautumiskohde" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Kriittiset sovellukset ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Ilmoitetut jäsenet ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Riskialttiit jäsenet" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Virheellinen pääsalasana" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Tiedoston salasana on virheellinen. Käytä vientitiedoston luonnin yhteydessä syötettyä salasanaa." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Näytettäviä kohteita ei ole." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "Sinulla ei ole kokoelman kaikkien kohteiden tarkastelun sallivia käyttöoikeuksia." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisaatio on jäädytetty" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Jäädytettyjen organisaatioiden kohteet eivät ole käytettävissä. Ole yhteydessä organisaatiosi omistajaan saadaksesi apua." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Palvelutilien luonti ei ole mahdollista jäädytetyissä organisaatioissa. Ole yhteydessä organisaatiosi omistajaan saadaksesi apua." }, - "disabledOrganizationFilterError": { - "message": "Jäädytettyjen organisaatioiden kohteet eivät ole käytettävissä. Ole yhteydessä organisaation omistajaan saadaksesi apua." - }, "licenseIsExpired": { "message": "Lisenssi on erääntynyt." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Kertakirjautumistunniste" }, - "ssoIdentifierHintPartOne": { - "message": "Toimita tämä tunniste jäsenillesi kertakirjautumista varten. Ohita vaihe määritämällä ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Poista kertakirjautumisliitos" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Pääsy estetty. Sinulla ei ole oikeutta avata sivua." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Ryhmä/käyttäjä" }, - "lowKdfIterations": { - "message": "Alhainen KDF-toistomäärä" - }, - "updateLowKdfIterationsDesc": { - "message": "Päivitä salausasetuksesi uusien suojaussuositusten mukaisiksi ja vahvista tilisi suojausta." - }, "kdfSettingsChangeLogoutWarning": { "message": "Jos jatkat, kirjataan kaikki aktiiviset istunnot ulos, jonka jälkeen sinun on kirjauduttava sisään uudelleen ja suoritettava mahdollisesti määritetty kaksivaiheinen tunnistautuminen. Tietojesi säilyvyyden varmistamiseksi suosittelemme, että viet holvisi sisällön ennen salausasetustesi muuttamista." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Määritä" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Määritä kokoelmiin" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Alhainen KDF-toistojen määrä. Paranna tilisi suojausta korottamalla määrää." - }, - "changeKDFSettings": { - "message": "Muuta KDF-asetuksia" - }, "secureYourInfrastructure": { "message": "Suojaa infrastruktuurisi" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 8bb6d1fa53c..96c97dcc6d5 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Hindi wasto ang master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Hindi wasto ang password ng file, mangyaring gamitin ang password na inilagay mo noong ginawa mo ang export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Walang maililistang item." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "Wala kang pahintulot na makita lahat ng mga item sa koleksyong ito." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisasyon ay suspindido." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Mga item sa mga naka-suspindong Organisasyon ay hindi ma-access. Mangyaring makipag-ugnayan sa may-ari ng iyong Organisasyon para sa tulong." - }, "licenseIsExpired": { "message": "Ang lisensya ay expired na." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Tagatukoy ng SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "I-unlink ang SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Hindi natanggap ang access. Wala kang pahintulot na tingnan ang pahinang ito." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Grupo/User" }, - "lowKdfIterations": { - "message": "Mababang KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index e0c6cf2287d..62fdc6741b2 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -59,8 +59,61 @@ "createNewLoginItem": { "message": "Créer un nouvel élément de connexion" }, - "criticalApplicationsActivityDescription": { - "message": "Une fois que vous avez marqué des applications comme critiques, elles s'afficheront ici." + "percentageCompleted": { + "message": "$PERCENT$% complété", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ sur $TOTAL$ tâches de sécurité terminées", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Progression du changement de mot de passe" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Affecter des tâches aux membres pour surveiller la progression" + }, + "onceYouReviewApplications": { + "message": "Une fois que vous avez évalué et marqué des applications comme critiques, elles s'afficheront ici." + }, + "sendReminders": { + "message": "Envoyer des rappels" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "Une fois que vous avez marqué les applications critiques, elles s'afficheront ici." + }, + "viewAtRiskMembers": { + "message": "Afficher les membres à risque" + }, + "viewAtRiskApplications": { + "message": "Afficher les applications à risque" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ sur $TOTAL$ applications critiques sont à risque en raison de mots de passe à risque", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } }, "criticalApplicationsWithCount": { "message": "Applications critiques ($COUNT$)", @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications à risque", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ mots de passe à risque", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Membres notifiés ($COUNT$)", "placeholders": { @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "Membres à risque" }, - "membersAtRiskActivityDescription": { - "message": "Membres pouvant modifier les éléments à risque pour les applications critiques" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Les membres ayant accès aux éléments à risque pour les applications critiques" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "$COUNT$ membres à risque", "placeholders": { "count": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Mot de passe principal invalide" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Mot de passe principal invalide. Confirmez que votre adresse courriel est correcte et que votre compte a été créé sur $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Mot de passe de fichier invalide, veuillez utiliser le mot de passe que vous avez entré lorsque vous avez créé le fichier d'exportation." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Aucun élément à afficher." }, + "noItemsInTrash": { + "message": "Aucun élément dans la corbeille" + }, + "noItemsInTrashDesc": { + "message": "Les éléments que vous supprimez apparaîtront ici et seront définitivement supprimés après 30 jours" + }, + "noItemsInVault": { + "message": "Aucun élément dans le coffre" + }, + "emptyVaultDescription": { + "message": "Le coffre protège bien plus que vos mots de passe. Stockez vos identifiants, IDs, cartes et notes en toute sécurité ici." + }, + "emptyFavorites": { + "message": "Vous n'avez mis aucun élément en favori" + }, + "emptyFavoritesDesc": { + "message": "Ajouter les éléments fréquemment utilisés aux favoris pour un accès rapide." + }, + "noSearchResults": { + "message": "Aucun résultat de recherche retourné" + }, + "clearFiltersOrTryAnother": { + "message": "Effacer les filtres ou essayer un autre terme de recherche" + }, "noPermissionToViewAllCollectionItems": { "message": "Vous n'avez pas l'autorisation d'afficher tous les éléments de cette collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "L'organisation est désactivée." }, + "organizationIsSuspended": { + "message": "L'organisation est suspendue" + }, + "organizationIsSuspendedDesc": { + "message": "Les éléments des organisations suspendues ne sont pas accessibles. Contactez le propriétaire de votre organisation pour obtenir de l'aide." + }, "secretsAccessSuspended": { "message": "Impossible d'accéder aux organisations suspendues. Veuillez contacter le propriétaire de votre organisation pour obtenir de l'aide." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Les comptes de service ne peuvent pas être créés dans les organisations suspendues. Veuillez contacter le propriétaire de votre organisation pour obtenir de l'aide." }, - "disabledOrganizationFilterError": { - "message": "Les éléments des Organisations désactivées ne sont pas accessibles. Contactez le propriétaire de votre Organisation pour obtenir de l'aide." - }, "licenseIsExpired": { "message": "La licence a expiré." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Identifiant SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Fournir cet ID à vos membres pour se connecter avec SSO. Pour contourner cette étape, configurer ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Fournir cet ID à vos membres pour se connecter avec SSO. Les membres peuvent passer la saisie de cet identifiant pendant SSO si un domaine revendiqué est configuré. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "En savoir plus", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Délier SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Seul le coffre de l'organisation associé à $ORGANIZATION$ sera exporté.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Seul le coffre de l'organisation associé à $ORGANIZATION$ sera exporté. Mes éléments de mes collections ne seront pas inclus.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Accès Refusé. Vous n'avez pas l'autorisation d'afficher cette page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Secret inconnu, vous devrez peut-être demander l'autorisation d'accéder à ce secret." }, + "unknownServiceAccount": { + "message": "Compte machine inconnu, vous devrez peut-être demander l'autorisation d'accéder à ce compte machine." + }, "unknownProject": { "message": "Projet inconnu, vous devrez peut-être demander l'autorisation d'accéder à ce projet." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Groupe/Utilisateur" }, - "lowKdfIterations": { - "message": "Itérations KDF basses" - }, - "updateLowKdfIterationsDesc": { - "message": "Mettez à jour vos paramètres de chiffrement pour répondre à de nouvelles recommandations de sécurité et améliorer la protection de votre compte." - }, "kdfSettingsChangeLogoutWarning": { "message": "Effectuer cette action vous déconnectera de toutes les sessions actives. Vous devrez vous connecter à nouveau et compléter votre authentification à 2 facteurs, s'il y a lieu. Nous vous recommandons d'exporter votre coffre avant de modifier vos paramètres de chiffrement pour prévenir la perte de vos données." }, @@ -8485,7 +8611,7 @@ } } }, - "accessedProjectWithId": { + "accessedProjectWithIdentifier": { "message": "A accédé à un projet avec l'identifiant : $PROJECT_ID$.", "placeholders": { "project_id": { @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "A supprimé le compte machine ID : $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "A modifié un projet avec l'identifiant : $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Utilisateur ajouté : $USER_ID$ au compte machine avec l'ID : $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "A supprimé l'utilisateur : $USER_ID$ du compte machine avec l'ID : $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "A supprimé le groupe : $GROUP_ID$ du compte machine avec l'ID : $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "A créé le compte machine avec l'ID : $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "A ajouté le groupe : $GROUP_ID$ du compte machine avec l'ID : $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "A supprimé le compte machine avec l'ID : $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "A supprimé un projet avec l'identifiant : $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assigner" }, + "assignTasks": { + "message": "Assigner des tâches" + }, "assignToCollections": { "message": "Assigner aux collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Impossible d'enregistrer l'intégration. Veuillez réessayer plus tard." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Vous devez être le propriétaire de l'organisation pour effectuer cette action." + }, "failedToDeleteIntegration": { "message": "La suppression de l'intégration échouée. Veuillez réessayer plus tard." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Nombres d'itérations KDF bas. Augmentez vos itérations pour améliorer la sécurité de votre compte." - }, - "changeKDFSettings": { - "message": "Modifier les paramètres KDF" - }, "secureYourInfrastructure": { "message": "Sécurisez votre infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Rechercher dans l'archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "Aucun élément dans l'archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "L'extension Bitwarden est installée !" }, + "openTheBitwardenExtension": { + "message": "Ouvrir l'extension Bitwarden" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "L'extension Bitwarden est installée! Ouvrez l'extension pour vous connecter et démarrer le remplissage automatique." + }, "openExtensionToAutofill": { "message": "Ouvrez l'extension pour vous connecter et démarrer le remplissage automatique." }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 8c163ef4f6a..c4c35278f4b 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 72a63ab210a..915145eebc5 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "צור פריט כניסה חדש" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "יישומים קריטיים ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "חברים שהודיעו להם ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "חברים בסיכון" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "סיסמה ראשית שגויה" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "סיסמת קובץ שגויה, נא להשתמש בסיסמה שהזנת כשיצרת את קובץ הייצוא." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "אין פריטים להצגה ברשימה." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "אין לך הרשאה להציג את כל הפריטים באוסף זה." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "הארגון הושעה" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "לא ניתן לגשת אל ארגונים מושעים. נא לפנות לבעל הארגון שלך עבור סיוע." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "לא ניתן ליצור חשבונות שירות בארגונים מושעים. נא לפנות אל בעל הארגון שלך עבור סיוע." }, - "disabledOrganizationFilterError": { - "message": "לא ניתן לגשת לפריטים בארגון מושעה. פנה אל בעל הארגון שלך עבור סיוע." - }, "licenseIsExpired": { "message": "תוקף הרשיון הסתיים." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "מזהה SSO" }, - "ssoIdentifierHintPartOne": { - "message": "ספק את המזהה הזה לחברים שלך כדי שיכנסו עם SSO. כדי לעקוף את השלב הזה, הגדר ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "נתק SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "גישה נדחתה. אין לך הרשאות כדי לצפות בעמוד זה." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "סוד לא ידוע, ייתכן שאתה צריך לבקש הרשאה כדי לגשת אל סוד זה." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "פרויקט לא ידוע, ייתכן שאתה צריך לבקש הרשאה כדי לגשת אל פרויקט זה." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "קבוצה/משתמש" }, - "lowKdfIterations": { - "message": "חזרות KDF נמוכות" - }, - "updateLowKdfIterationsDesc": { - "message": "שדרג את הגדרות ההצפנה שלך כדי לעמוד בהמלצות אבטחה חדשות ולשפר את הגנת החשבון." - }, "kdfSettingsChangeLogoutWarning": { "message": "המשך התהליך יוציא אותך מכל ההפעלות הפעילות שלך. תידרש להיכנס חזרה כדי להמשיך כניסה דו-שלבית, אם ישנה. אנו ממליצים על ייצוא הכספת שלך לפני שינוי הגדרות ההצפנה שלך כדי למנוע איבוד נתונים." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "ניגש אל פרויקט עם מזהה: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "ערך פרויקט עם מזהה: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "מחק פרויקט עם מזהה: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "הקצה" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "הקצה לאוספים" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "שמירת האינטגרציה נכשלה. נא לנסות שוב מאוחר יותר." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "חזרות KDF נמוכות. הגדל את מספר החזרות שלך כדי לשפר את האבטחה של חשבונך." - }, - "changeKDFSettings": { - "message": "שנה הגדרות KDF" - }, "secureYourInfrastructure": { "message": "אבטח את התשתית שלך" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "הרחבת Bitwarden הותקנה!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "פתח את ההרחבה כדי להיכנס ולהתחיל למלא אוטומטית." }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 9cdcabf7423..f7d116162ff 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 66eca3f69a9..8f49041638a 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -59,8 +59,61 @@ "createNewLoginItem": { "message": "Stvori novu stavku prijave" }, - "criticalApplicationsActivityDescription": { - "message": "Aplikacije označene kao kritične će biti prikazane ovdje." + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "Once you mark applications critical, they will display here." + }, + "viewAtRiskMembers": { + "message": "Rizični korisnici" + }, + "viewAtRiskApplications": { + "message": "Rizične aplikacije" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ od $TOTAL$ kritilčnih aplikacija su ugrožene zbog rizičnih lozinki", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } }, "criticalApplicationsWithCount": { "message": "Kritične aplikacije ($COUNT$)", @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ aplikacija/e ugroženo", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Obaviješteni članovi ($COUNT$)", "placeholders": { @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "Rizični korisnici" }, - "membersAtRiskActivityDescription": { - "message": "Članovi koji mogu uređivati stavke za aplikacije označene kao kritične" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "Rizičnih članova: $COUNT$", "placeholders": { "count": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Neispravna glavna lozinka" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Nevažeća glavna lozinka. Provjeri je li tvoja adresa e-pošta ispravna i je li račun kreiran na $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Nesipravna lozinka datoteke. Unesi lozinku izvozne datoteke." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Nema stavki za prikaz." }, + "noItemsInTrash": { + "message": "Nema stavki u smeću" + }, + "noItemsInTrashDesc": { + "message": "Stavke koje obrišeš biti će premještene ovdje, a nakon 30 dana biti će trajno izbrisane" + }, + "noItemsInVault": { + "message": "Nema stavaka u trezoru" + }, + "emptyVaultDescription": { + "message": "Trezor štiti više od lozinki. Sigurno spremi prijave, identitete, kartice i bilješke." + }, + "emptyFavorites": { + "message": "Niti jedna stavka nije dodana u favorite" + }, + "emptyFavoritesDesc": { + "message": "Za brzi pristup često korištenim stavkama dodaj ih u favorite." + }, + "noSearchResults": { + "message": "Nema rezultata pretrage" + }, + "clearFiltersOrTryAnother": { + "message": "Očisti filtre ili pokušaj s drugačijom pretragom" + }, "noPermissionToViewAllCollectionItems": { "message": "Nemaš prava vidjeti sve stavke u ovoj zbirci." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organizacija suspendirana" }, + "organizationIsSuspended": { + "message": "Organizacija je suspendirana" + }, + "organizationIsSuspendedDesc": { + "message": "Stavkama u suspendiranoj Organizaciji se ne može pristupiti. Kontaktiraj vlasnika Organizacije za pomoć." + }, "secretsAccessSuspended": { "message": "Stavkama u suspendiranoj Organizaciji se ne može pristupiti. Kontaktiraj vlasnika Organizacije za pomoć." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Nije moguće stvoriti Servisne račune u suspendiranim organizacijama. Kontaktiraj vlasnika tvoje organizacije za pomoć." }, - "disabledOrganizationFilterError": { - "message": "Stavkama u suspendiranoj Organizaciji se ne može pristupiti. Kontaktiraj vlasnika Organizacije za pomoć." - }, "licenseIsExpired": { "message": "Licenca je istekla." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifikator" }, - "ssoIdentifierHintPartOne": { - "message": "Dajte ovaj ID svojim članovima za prijavu putem SSO-a. Za zaobilazak ovog koraka, postavi ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Daj ovaj ID svojim članovima za prijavu putem SSO-a. Članovi mogu preskočiti unos ovog identifikatora tijekom SSO-a ako je postavljena domena za koju su prijavljeni. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Saznaj više", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Odspoji SSO" @@ -5558,10 +5669,10 @@ "message": "Pravila tvrtke onemogućuju spremanje stavki u osobni trezor. Promijeni vlasništvo stavke na tvrtku i odaberi dostupnu Zbirku." }, "desktopAutotypePolicy": { - "message": "Desktop Autotype Default Setting" + "message": "Zadana postavka automatskog tipkanja na radnoj površini" }, "desktopAutotypePolicyDesc": { - "message": "Turn Desktop Autotype ON by default for members. Members can turn Autotype off manually in the Desktop client.", + "message": "Uključi automatsko tipkanje na radnoj površini prema zadanim postavkama za članove. Članovi mogu ručno isključiti automatsko tipkanje u klijentu za radnu površinu.", "description": "This policy will enable Desktop Autotype by default for members on Unlock." }, "disableSend": { @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Izvezt će se samo trezor organizacije povezan s $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Izvezt će se samo trezor organizacije povezan s $ORGANIZATION$. Zbirka mojih stavki neće biti uključena.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Pristup odbijen. Nemaš prava vijdeti ovu stranicu." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Nepoznata tajna. Možda trebaš tražiti dozvolu za pristup ovoj tajni." }, + "unknownServiceAccount": { + "message": "Nepoznati mašinski račun. Možda trebaš tražiti dozvolu za pristup ovom mašinskom računu." + }, "unknownProject": { "message": "Nepoznati projekt. Možda trebaš tražiti dozvolu za pristup ovom projektu." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Grupa/korisnik" }, - "lowKdfIterations": { - "message": "Niske KDF iteracije" - }, - "updateLowKdfIterationsDesc": { - "message": "Ažuriraj svoje postavke enkripcije kako bi zadovoljili nove sigurnosne preporuke i poboljšali zaštitu računa." - }, "kdfSettingsChangeLogoutWarning": { "message": "Nastavkom ćeš se odjaviti iz svih aktivnih sesija. Morat ćeš se ponovno prijaviti i izvršiti dvostruku autentifikaciju, ako je aktivna. Preporučujemo izvoz tvog trezora prije promjene postavki enkripcije kako bi spriječili gubitak podataka." }, @@ -8485,7 +8611,7 @@ } } }, - "accessedProjectWithId": { + "accessedProjectWithIdentifier": { "message": "Pristupljeno projektu s identifikatorom: $PROJECT_ID$.", "placeholders": { "project_id": { @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Obrisan mašinski račun s id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Uređen projekt s identifikatorom: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Korisnik: $USER_ID$ dodan mašinskom računu $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Korisnik $USER_ID$ uklonjen iz mašinskog računa $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Grupa $GROUP_ID$ uklonjena iz mašinskog računa$SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Stvoren mašinski račun $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Grupa $GROUP_ID$ dodana mašinskom računu $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Obrisan mašinski račun $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Izbrisan projekt s identifkatorom: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Dodijeli" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Dodijeli zbirkama" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Spremanje integracije nije uspjelo. Pokušaj ponovno kasnije." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Za ovo, moraš biti vlasnik organizacije." + }, "failedToDeleteIntegration": { "message": "Brisanje integracije nije uspjelo. Pokušaj ponovno kasnije." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Niske KDF iteracije. Povećaj svoje iteracije za poboljšanje sigurnost računa." - }, - "changeKDFSettings": { - "message": "Promijeni KDF postavke" - }, "secureYourInfrastructure": { "message": "Osiguraj svoju infrastrukturu" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Pretraži arhivu" }, - "archive": { - "message": "Arhiva" + "archiveNoun": { + "message": "Arhiva", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arhiviraj", + "description": "Verb" }, "noItemsInArchive": { "message": "Nema stavki u arhivi" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden proširenje je instalirano!" }, + "openTheBitwardenExtension": { + "message": "Otvori Bitwarden proširenje" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "Instalirano je Bitwarden proširenje! Otvori ga za prijavu i početak auto-ispune." + }, "openExtensionToAutofill": { "message": "Otvori proširenje i prijavi se za početak korištenja auto-ispune." }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 910447f236c..cfc6e9e0735 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -59,8 +59,61 @@ "createNewLoginItem": { "message": "Új bejelentkezési elem létrehozása" }, - "criticalApplicationsActivityDescription": { - "message": "Miután megjelöltük a kritikus alkalmazásokat, azok itt jelennek meg." + "percentageCompleted": { + "message": "$PERCENT$% kész", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ / $TOTAL$ biztonsági feladatok elvégzésre került.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Jelszócsere feldolgozás" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Rendeljünk a tagokhoz feladatokat az előrehaladás monitorozására." + }, + "onceYouReviewApplications": { + "message": "Az alkalmazások áttekintése és kritikusnak jelölése után ezek itt jelennek meg." + }, + "sendReminders": { + "message": "Emlékeztető küldés" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "A kritikus alkalmazások megjelölésével azok itt jelennek meg." + }, + "viewAtRiskMembers": { + "message": "Kockázatos tagok megtekintése" + }, + "viewAtRiskApplications": { + "message": "Kockázatos alkalmazások megtekintése" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ / $TOTAL$ kritikus alkalmazás veszélyben van a kockázatos jelszavak miatt.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } }, "criticalApplicationsWithCount": { "message": "Kritikus alkalmazások ($COUNT$)", @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ kockázatos alkalmazás", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ kockázatos jelszó", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Értesített tagok ($COUNT$)", "placeholders": { @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "Veszélyes tagok" }, - "membersAtRiskActivityDescription": { - "message": "Tagok szerkesztési hozzáféréssel a kritikus alkalmazások veszélyeztetett elemeihez" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "$COUNT$ kockázatos tag", "placeholders": { "count": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "A mesterjelszó érvénytelen." }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "A mesterjelszó érvénytelen. Erősítsük meg, hogy email cím helyes és a fiók létrehozásának helye: $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Érvénytelen a fájl jelszó. Használjuk az exportálás fájl létrehozásakor megadott jelszót." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Nincsenek megjeleníthető elemek." }, + "noItemsInTrash": { + "message": "Nincs elem a lomtárban." + }, + "noItemsInTrashDesc": { + "message": "A törölt elemek itt jelennek meg és 30 nap elteltével véglegesen törlődnek." + }, + "noItemsInVault": { + "message": "Nincsenek elemek a széfben." + }, + "emptyVaultDescription": { + "message": "A széf nemcsak a jelszavakat védi. Itt biztonságosan tárolhatjuk a bejelentkezési adatokat, egyéb azonosítókat, kártyákat és jegyzeteket." + }, + "emptyFavorites": { + "message": "Nem lett kedvencnek minősítve egyetlen elem sem." + }, + "emptyFavoritesDesc": { + "message": "Gyakran használt elemek hozzáadása a kedvencekhez a gyors hozzáférés érdekében." + }, + "noSearchResults": { + "message": "Nincsenek visszakapott keresési eredmények." + }, + "clearFiltersOrTryAnother": { + "message": "Töröljük a szűrőket vagy próbálkozzunk másik keresési kifejezéssel." + }, "noPermissionToViewAllCollectionItems": { "message": "Nincs jogosultság a gyűjtemény összes elemének megtekintésére." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "A szervezet letiltásra került." }, + "organizationIsSuspended": { + "message": "A szervezet felfüggesztésre került." + }, + "organizationIsSuspendedDesc": { + "message": "A letiltott szervezetek elemei nem érhetők el. Vegyük fel a kapcsolatot a szervezet tulajdonosával segítségért." + }, "secretsAccessSuspended": { "message": "A felfüggesztett szervezetekhez nem lehet hozzáférni. Segítségért forduljunk a szervezet tulajdonosához." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "A felfüggesztett szervezetekben nem hozhatók létre szolgáltatásfiókok. Segítségért forduljunk a szervezet tulajdonosához." }, - "disabledOrganizationFilterError": { - "message": "A letiltott szervezetek elemei nem érhetők el. Vegyük fel a kapcsolatot a szervezet tulajdonosával segítségért." - }, "licenseIsExpired": { "message": "A licensz lejárt." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Egyszeri azonosító" }, - "ssoIdentifierHintPartOne": { - "message": "Adjuk meg ezt az azonosítót a tagoknak a bejelentkezéshez SSO-val. A lépés megkerüléséhez üzemeljük be ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Adjuk meg ezt az azonosítót a tagokinak az SSO-val bejelentkezéshez. A tagok kihagyhatják ennek az azonosítónak a megadását az SSO során, ha egy igényelt tartomány be van állítva.", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "További információ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "SSO szétkapcsolása" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Csak a $ORGANIZATION$ szervezetehez kapcsolódó szervezeti széf kerül exportálásra.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Csak a $ORGANIZATION$ szervezethez kapcsolódó szervezeti széf kerül exportálásra. A saját elem gyűjtemények nem lesznek benne.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "A hozzáférés megtagadásra került. Nincs jogosultság az oldal megtekintésére." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Ismeretlen a titkos kód, előfordulhat, hogy engedélyt kell kérni a titkos kód eléréséhez." }, + "unknownServiceAccount": { + "message": "A gép fiók ismeeretlen, előfordulhat, hogy engedélyt kell kérni a gép fiók eléréséhez." + }, "unknownProject": { "message": "Ismeretlen a projekt, előfordulhat, hogy engedélyt kell kérni a projekt eléréséhez." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Csoport/Felhasználó" }, - "lowKdfIterations": { - "message": "Alacsony KDF iterációk" - }, - "updateLowKdfIterationsDesc": { - "message": "Frissítsük a titkosítási beállításokat, hogy megfeleljünk az új biztonsági ajánlásoknak és javítsuk a fiókvédelmet." - }, "kdfSettingsChangeLogoutWarning": { "message": "A folytatás kijelentkeztet az összes aktív munkamenetből. Újra be kell jelentkezni és kétlépcsős bejelentkezést kell végrehajtani, ha van ilyen. Célszerű a titkosítási beállítások módosítása előtt a széf exportálása az adatvesztés elkerülése érdekében." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Egy projekt hozzáférésre került egy azonosítóval: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Egy projekt elérése megtörtént egy azonosítóval: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "A gép fiók törlésre került egy azonosítóval: $SERVICE_ACCOUNT_ID$.", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Egy projekt szerkesztése megtörtént egy azonosítóval: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Egy felhasználó került hozzáadásra: $USER_ID$ a gépi fiókhoz egy azonosítóval: $SERVICE_ACCOUNT_ID$.", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Egy felhasználó lett eltávolítva: $USER_ID$ a gépi fiókból egy azonosítóval: $SERVICE_ACCOUNT_ID$.", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Egy csoport lett eltávolítva: $GROUP_ID$ a gépi fiókból egy azonosítóval: $SERVICE_ACCOUNT_ID$.", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Egy gép fiók lett létrehozva egy azonosítóval: $SERVICE_ACCOUNT_ID$.", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Egy csoport lett hozzáadva: $GROUP_ID$ a gép fiókhoz egy azonosítóval: $SERVICE_ACCOUNT_ID$.", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Egy gép fiók került tötlésre egy azonosítóval: $SERVICE_ACCOUNT_ID$.", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Egy projekt törlése megtörtént egy azonosítóval: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Hozzárendelés" }, + "assignTasks": { + "message": "Feladatok hozzárendelése" + }, "assignToCollections": { "message": "Hozzárendelés gyűjteményekhez" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Nem sikerült menteni az integrációt. Próbáljuk újra később." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Ennek a műveletnek a végrehajtásához a szervezet tulajdonosának kell lenni." + }, "failedToDeleteIntegration": { "message": "Nem sikerült törölni az integrációt. Próbáljuk újra később." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Alacsony a KDF iterációk száma. Növeljük az iterációk számát a fiók biztonságának javítása érdekében." - }, - "changeKDFSettings": { - "message": "KDF beállítások megváltoztatása" - }, "secureYourInfrastructure": { "message": "Infrastruktúra biztosítása" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Keresés archívum" }, - "archive": { - "message": "Archívum" + "archiveNoun": { + "message": "Archívum", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archívum", + "description": "Verb" }, "noItemsInArchive": { "message": "Nincs elem az archívumban." @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "A Bitwarden bővítmény megnyitása" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "A Bitwarden bővítmény telepítve van! Nyissuk meg a bővítményt a bejelentkezéshez és az automatikus kitöltés megkezdéséhez." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 39f4232a707..3a628b0889b 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Buat entri masuk baru" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Semua aplikasi ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Anggota yang diberitahukan ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Sandi utama tidak valid" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Kata sandi berkas tidak valid, harap menggunakan kata sandi yang anda masukkan saat anda membuat berkas ekspor." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Tidak ada item yang dapat dicantumkan." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisasi dinonaktifkan." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Item di Organisasi yang dinonaktifkan tidak bisa diakses. Hubungi pemilik Organisasi Anda untuk mendapatkan bantuan." - }, "licenseIsExpired": { "message": "Lisensi sudah kadaluarsa." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Batalkan tautan SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Akses ditolak. Anda tidak mempunyai izin untuk melihat halaman ini." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Kelompok/Pengguna" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 3875bcb42f2..53eece391a5 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Crea nuovo elemento di login" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Applicazioni critiche ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Membri notificati ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Membri a rischio" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Password principale errata" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Password errata, usa la password che hai inserito alla creazione del file di esportazione." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Non ci sono elementi da mostrare." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "Non hai i permessi necessari per visualizzare tutti gli elementi in questa raccolta." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organizzazione disabilitata" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Non è possibile accedere alle organizzazioni disabilitate. Contatta il proprietario della tua organizzazione per assistenza." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Non è possibile creare account di servizio nelle organizzazioni disabilitate. Contatta il proprietario della tua organizzazione per assistenza." }, - "disabledOrganizationFilterError": { - "message": "Non è possibile accedere agli elementi nelle organizzazioni disabilitate. Contatta il proprietario della tua organizzazione per assistenza." - }, "licenseIsExpired": { "message": "La licenza è scaduta." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Identificativo SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Fornisci questo ID ai tuoi membri per accedere con SSO. Per saltare questo passaggio, configura la ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Scollega SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Accesso negato. Non hai i permessi necessari per visualizzare questa pagina." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Segreto sconosciuto, potrebbe essere necessario richiedere l'autorizzazione per l'accesso." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Progetto sconosciuto, potrebbe essere necessario richiedere l'autorizzazione per l'accesso." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Gruppo/Utente" }, - "lowKdfIterations": { - "message": "Iterazioni KDF basse" - }, - "updateLowKdfIterationsDesc": { - "message": "Aggiorna le tue impostazioni di crittografia per soddisfare le nuove raccomandazioni sulla sicurezza e migliorare la protezione del tuo account." - }, "kdfSettingsChangeLogoutWarning": { "message": "Procedere ti farà uscire da tutte le sessioni attive. Dovrai accedere di nuovo e completare la verifica in due passaggi, se impostata. Ti consigliamo di esportare la tua cassaforte prima di cambiare le impostazioni di crittografia per prevenire perdite di dati." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accesso al progetto con identificatore $PROJECT_ID$ riuscito.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Progetto con identificatore $PROJECT_ID$ modificato", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Progetto con identificatore $PROJECT_ID$ eliminato", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assegna" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assegna alle raccolte" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Impossibile salvare l'integrazione. Riprova più tardi." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Iterazioni KDF basse. Aumenta le tue iterazioni per migliorare la sicurezza del tuo account." - }, - "changeKDFSettings": { - "message": "Cambia impostazioni KDF" - }, "secureYourInfrastructure": { "message": "Proteggi la tua infrastruttura" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Estensione di Bitwarden installata!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Apri l'estensione cliccando sul tasto della barra degli strumenti e accedi con i tuoi dati per attivare il riempimento automatico." }, diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 3f2a2ada371..bd8a9465b7b 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "新しいログインアイテムを作成" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "特に重要なアプリ ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "通知済みメンバー ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "リスクがあるメンバー" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "マスターパスワードが間違っています" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "無効なファイルパスワードです。エクスポートファイルを作成したときに入力したパスワードを使用してください。" }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "表示するアイテムがありません" }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "このコレクション内のアイテムをすべて表示する権限がありません。" }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "組織は無効です。" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "一時停止された組織にはアクセスできません。組織の所有者にお問い合わせください。" }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "一時停止中の組織ではサービスアカウントを作成できません。組織の所有者に問い合わせてください。" }, - "disabledOrganizationFilterError": { - "message": "無効な組織のアイテムにアクセスすることはできません。組織の所有者に連絡してください。" - }, "licenseIsExpired": { "message": "ライセンスの有効期限が切れています。" }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO 識別子" }, - "ssoIdentifierHintPartOne": { - "message": "SSO でログインできるようこの ID をメンバーに提供してください。この手順をバイパスするには、以下の設定をしてください:", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "SSO のリンクを解除" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "アクセスが拒否されました。このページを表示する権限がありません。" }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "グループ/ユーザー" }, - "lowKdfIterations": { - "message": "低 KDF イテレーション" - }, - "updateLowKdfIterationsDesc": { - "message": "新しいセキュリティの推奨事項に対応し、アカウントの保護を向上させるために、暗号化設定を更新してください。" - }, "kdfSettingsChangeLogoutWarning": { "message": "続行すると、すべてのアクティブなセッションからログアウトします。再度ログインし、2段階認証を完了する必要があります。 暗号化設定を変更する前に、保管庫をエクスポートしてデータの損失を防ぐことをおすすめします。" }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "割り当て" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "コレクションに割り当てる" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "KDF 反復回数が少ないです。あなたのアカウントのセキュリティを向上させるために反復回数を増やしてください。" - }, - "changeKDFSettings": { - "message": "KDF 設定の変更" - }, "secureYourInfrastructure": { "message": "インフラストラクチャの保護" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 170b989ff5d..a57142dc31f 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "არასწორი მთავარი პაროლი" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "არასწორი ფაილის პაროლი, გთხოვთ გამოიყენოთ პაროლი რომელიც თქვენ შეიყვანეთ როცა შექმენით ექსპორტ ფაილი." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "არაა საგნები სიაში ჩამოსათველალდ." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "თქვენ არ გაქვთ უფლება ნახოთ ყველა საგანი ამ კოლექციაში." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 526b7567d99..173520dab61 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 13f72bed6c0..d625ab92f07 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "ಅಮಾನ್ಯ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "ಪಟ್ಟಿ ಮಾಡಲು ಯಾವುದೇ ಐಟಂಗಳಿಲ್ಲ." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "ಸಂಘಟನೆಯನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "ಪರವಾನಗಿ ಅವಧಿ ಮೀರಿದೆ." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "ಎಸ್‌ಎಸ್‌ಒ ಅನ್ಲಿಂಕ್ ಮಾಡಿ" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 33ccaee2b5c..2ebd08b1733 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "잘못된 마스터 비밀번호" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "파일 암호가 잘못되었습니다. 내보낸 파일을 생성했을 때 사용한 암호를 입력하세요." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "항목이 없습니다." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "이 컬렉션의 모든 항목을 볼 권한이 없습니다." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "조직이 비활성화됨" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "라이선스가 만료되었습니다." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "SSO 연결 해제" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index bdf7dbef0fb..28bca33cdd5 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Izveidot jaunu pieteikšanās vienumu" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Tiklīz lietotnes tiks atzīmētas kā būtiskas, tās tiks parādītas šeit." }, + "viewAtRiskMembers": { + "message": "Apskatīt riskam pakļautos dalībniekus" + }, + "viewAtRiskApplications": { + "message": "Apskatīt riskam pakļautās lietotnes" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ no $TOTAL$ būtiskajām lietotnēm ir apdraudētas riskam pakļautu paroļu dēļ", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Kritiskās lietotnes ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ lietotnes ir pakļautas riskam", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Apziņotie dalībnieki ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Riskam pakļautie dalībnieki" }, - "membersAtRiskActivityDescription": { - "message": "Dalībnieki ar labošanas piekluvi riskam pakļautajiem vienumiem būtiskajām lietotnēm" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Dalībnieki ar piekluvi riskam pakļautajiem vienumiem būtiskajām lietotnēm" }, - "membersAtRisk": { - "message": "$COUNT$ riskam pakļauti dalībnieki", + "membersAtRiskCount": { + "message": "$COUNT$ dalībnieki ir pakļauti riskam", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Nederīga galvenā parole" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Nederīga galvenā parole. Jāpārliecinās, ka e-pasta adrese ir pareiza un konts tika izveidots $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Nederīga datnes parole, lūgums izmantot to paroli, kas tika ievadīta izgūšanas datnes izveidošanas brīdī." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Nav vienumu, ko parādīt." }, + "noItemsInTrash": { + "message": "Atkritnē nav vienumu" + }, + "noItemsInTrashDesc": { + "message": "Izdzēstie vienumi parādīsies šeit, un tie tiks neatgriezeniski izdzēsti pēc 30 dienām" + }, + "noItemsInVault": { + "message": "Glabātavā nav vienumu" + }, + "emptyVaultDescription": { + "message": "Glabātava aizsargā vairāk kā tikai paroles. Drošā veidā glabā šeit pieteikšanās vienumus, identitātes, kartes un piezīmes!" + }, + "emptyFavorites": { + "message": "Izlasē nav neviena vienuma" + }, + "emptyFavoritesDesc": { + "message": "Šeit ir pievienojami bieži izmantoti vienumi ātrākai piekļuvei." + }, + "noSearchResults": { + "message": "Nekas netika atrasts" + }, + "clearFiltersOrTryAnother": { + "message": "Jānotīra atsijātāji vai jāmēģina cits meklēšanas vaicājums" + }, "noPermissionToViewAllCollectionItems": { "message": "Nav atļaujas apskatīt visus šī krājuma vienumus." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Apvienība ir atspējota." }, + "organizationIsSuspended": { + "message": "Apvienība ir apturēta" + }, + "organizationIsSuspendedDesc": { + "message": "Apturētu apvienību vienumiem nevar piekļūt. Jāsazinās ar apvienības īpašnieku, lai iegūtu palīdzību." + }, "secretsAccessSuspended": { "message": "Apturētām apvienībām nav iespējams piekļūt. Lūgums vērsties pie savas apvienības īpašnieka pēc palīdzības." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Pakalpojumu kontus nav iespējams izveidot apturētās apvienībās. Lūgums vērsties pie savas apvienības īpašnieka pēc palīdzības." }, - "disabledOrganizationFilterError": { - "message": "Atspējotu apvienību vienumiem nevar piekļūt. Jāsazinās ar apvienības īpašnieku, lai iegūtu palīdzību." - }, "licenseIsExpired": { "message": "Ir beidzies licences izmantošanas laiks." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Vienotās pieteikšanās identifikators" }, - "ssoIdentifierHintPartOne": { - "message": "Šis Id ir sniedzams dalībniekiem, lai pieteiktos ar vienoto pieteikšanos. Lai apietu šo soli, jāuzstāda ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Atsaistīt SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai apskatītu šo lapu." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Nezināms noslēpums, var būt nepieciešams pieprasīt atļauju piekļūt tam." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Nezināms projekts, var būt nepieciešams pieprasīt atļauju piekļūt tam." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Kopa/Lietotājs" }, - "lowKdfIterations": { - "message": "Zems KDF atkārtojumu skaits" - }, - "updateLowKdfIterationsDesc": { - "message": "Jāatjaunina šifrēšanas iestatījumi, lai atbilstu jaunajiem drošības ieteikumiem un uzlabotu konta aizsardzību." - }, "kdfSettingsChangeLogoutWarning": { "message": "Turpinot notiks atteikšanās no visām esošajām sesijām. Būs atkārtoti jāpiesakās un jāpabeidz divpakāpju pieteikšanās, ja tāda ir. Mēs iesakām pirms šifrēšanas iestatījumu mainīšanas izgūt glabātavas saturu, lai novērstu datu zudumu." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Piekļuva projektam ar Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Laboja projektu ar identifikatoru: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Izdzēsa projektu ar identifikatoru: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Piešķirt" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Piešķirt krājumiem" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Neizdevās saglabāt iekļaušanu. Lūgums vēlāk mēģināt vēlreiz." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Jābūt apvienības īpašniekam, lai veiktu šo darbību." + }, "failedToDeleteIntegration": { "message": "Neizdevās izdzēst iekļaušanu. Lūgums vēlāk mēģināt vēlreiz." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Zems KDF atkārtojumu skaits. Tas jāpalielina, lai uzlabotu sava konta drošību." - }, - "changeKDFSettings": { - "message": "Mainīt KDF iestatījumus" - }, "secureYourInfrastructure": { "message": "Nodrošini savu infrastruktūru" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Meklēt arhīvā" }, - "archive": { - "message": "Arhivēt" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "Arhīvā nav vienumu" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden paplašinājums uzstādīts." }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Jāatver paplašinājums, lai pieteiktos un uzsāktu automātisko aizpildi." }, diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 7f345a5b85c..cbb5aeecf49 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "അസാധുവായ പ്രാഥമിക പാസ്‌വേഡ്" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "പ്രദർശിപ്പിക്കാൻ ഇനങ്ങളൊന്നുമില്ല." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "സംഘടന അപ്രാപ്‌തമാക്കി." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "ലൈസൻസ് കാലഹരണപ്പെട്ടു." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index b25c6fb16ea..2ea56d367ff 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "नवीन लॉगिन आयटम तयार करा" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 526b7567d99..173520dab61 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 3cfcd87ec64..778d3adf74c 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Ugyldig hovedpassord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Ugyldig filpassord, bruk passordet du skrev inn da du opprettet eksportfilen." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Det er ingen elementer å vise." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "Du har ikke tillatelse til å se alle elementer i denne samlingen." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisasjonen er skrudd av." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Gjenstander i deaktiverte organisasjoner kan ikke åpnes. Ta kontakt med eieren av organisasjonen for hjelp." - }, "licenseIsExpired": { "message": "Lisensen har utløpt." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO-identifikator" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Koble fra SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Ingen tilgang. Du har ikke tillatelse til å se denne siden." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Gruppe/Bruker" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Knytt" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Legg til i samlinger" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 7dff2786a5d..6575f687031 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 1ad5b20745a..31d648fb94f 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Nieuw login item aanmaken" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% compleet", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ van $TOTAL$ beveiligingstaken voltooid", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Voortgang veranderen wachtwoord" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Leden taken toewijzen om de voortgang te controleren" + }, + "onceYouReviewApplications": { "message": "Als je toepassingen als belangrijk markeert, verschijnen ze hier." }, + "sendReminders": { + "message": "Herinneringen versturen" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "Als je toepassingen als belangrijk markeert, verschijnen ze hier." + }, + "viewAtRiskMembers": { + "message": "Leden met risico bekijken" + }, + "viewAtRiskApplications": { + "message": "Applicaties met risico bekijken" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ van de $TOTAL$ kritische applicaties zijn in gevaar als gevolg van risicovolle wachtwoorden", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Belangrijke applicaties ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applicaties in gevaar", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ wachtwoorden lopen risico", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Geînformeerde leden ($COUNT$)", "placeholders": { @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "Leden in gevaar" }, - "membersAtRiskActivityDescription": { - "message": "Leden met toegang voor bewerken van risico-items voor belangrijke applicaties" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Leden met toegang tot risico-items voor belangrijke applicaties" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "$COUNT$ leden lopen risico", "placeholders": { "count": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Ongeldig hoofdwachtwoord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ongeldig hoofdwachtwoord. Check of je e-mailadres klopt en of je account is aangemaakt op $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Onjuist bestandswachtwoord, gebruik het wachtwoord dat je hebt ingevoerd bij het aanmaken van het exportbestand." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Er zijn geen items om weer te geven." }, + "noItemsInTrash": { + "message": "Geen items in prullenbak" + }, + "noItemsInTrashDesc": { + "message": "Items die je verwijdert verschijnen hier en worden na 30 dagen definitief verwijderd" + }, + "noItemsInVault": { + "message": "Geen items in de kluis" + }, + "emptyVaultDescription": { + "message": "De kluis beschermt meer dan alleen je wachtwoorden. Sla hier beveiligde inloggegevens, ID's, kaarten en notities op." + }, + "emptyFavorites": { + "message": "Je hebt geen favoriete items" + }, + "emptyFavoritesDesc": { + "message": "Voeg veelgebruikte items toe aan favorieten voor snelle toegang." + }, + "noSearchResults": { + "message": "Geen resultaten teruggekregen" + }, + "clearFiltersOrTryAnother": { + "message": "Wis filters of probeer een andere zoekterm" + }, "noPermissionToViewAllCollectionItems": { "message": "Je hebt geen rechten om alle items in deze collectie te zien." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisatie opgeschort" }, + "organizationIsSuspended": { + "message": "Organisatie is opgeschort" + }, + "organizationIsSuspendedDesc": { + "message": "Je kunt items in opgeschorte organisaties niet benaderen. Neem contact op met de eigenaar van je organisatie voor hulp." + }, "secretsAccessSuspended": { "message": "Opgeschorte organisaties zijn niet toegankelijk. Neem contact op met de eigenaar van je organisatie voor hulp." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Je kunt geen service accounts aanmaken in opgeschorte organisaties. Neem contact op met de eigenaar van je organisatie voor hulp." }, - "disabledOrganizationFilterError": { - "message": "Je kunt uitgeschakelde items in een organisatie niet benaderen. Neem contact op met de eigenaar van je organisatie voor hulp." - }, "licenseIsExpired": { "message": "Licentie verlopen." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO Identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Geef dit ID aan je leden voor inloggen met SSO. Om deze stap te omzeilen, configureer je ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Geef dit ID aan je leden om in te loggen met SSO. Leden kunnen het invoeren van deze identificatie overslaan tijdens SSO als een geclaimde domein is ingesteld ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Meer informatie", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "SSO ontkoppelen" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Alleen de organisatiekluis die gekoppeld is aan $ORGANIZATION$ wordt geëxporteerd.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Exporteert alleen de organisatiekluis van $ORGANIZATION$. Geen persoonlijke kluis-items.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Toegang geweigerd. Je hebt geen toestemming om deze pagina te bekijken." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Onbekend secret, je moet misschien toegang vragen om dit secret te benaderen." }, + "unknownServiceAccount": { + "message": "Onbekend machine-account, je moet misschien toegang vragen om dit machine-acount te benaderen." + }, "unknownProject": { "message": "Onbekend project, je moet misschien toegang vragen om dit project te benaderen." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Groep/Gebruiker" }, - "lowKdfIterations": { - "message": "Weinig KDF-iteraties" - }, - "updateLowKdfIterationsDesc": { - "message": "Werk je versleutelingsinstellingen bij om aan de nieuwe beveiligingsaanbevelingen te voldoen en de bescherming van je account te verbeteren." - }, "kdfSettingsChangeLogoutWarning": { "message": "Als je doorgaat, log je uit van alle actieve sessies. Je zult opnieuw moeten inloggen en, indien van toepassing, tweestapsverificatie moeten voltooien. We raden aan om je kluis te exporteren voordat je je versleutelingsinstellingen wijzigt om gegevensverlies te voorkomen." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Project gekoppeld met Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Project bekeken met identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Machine-account ID verwijderd: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Project bewerkt met identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Gebruiker toegevoegd: $USER_ID$ naar machine-account met identificatie: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Gebruiker verwijderd: $USER_ID$ van machine-account met identificatie: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Groep verwijderd: $USER_ID$ van machine-account met identificatie: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Machine-account aangemaakt met identificatie: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Groep toegevoegd: $GROUP_ID$ van machine-account met identificatie: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Machine-account verwijderd met identificatie: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Project verwijderd met identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Toewijzen" }, + "assignTasks": { + "message": "Taken toewijzen" + }, "assignToCollections": { "message": "Toewijzen aan collecties" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Opslaan van integratie mislukt. Probeer het later opnieuw." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Je moet de eigenaar van de organisatie zijn om deze actie uit te voeren." + }, "failedToDeleteIntegration": { "message": "Verwijderen van integratie mislukt. Probeer het later opnieuw." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Laag aantal KDF-iteraties. Verhoog je iteraties om de veiligheid van je account te verbeteren." - }, - "changeKDFSettings": { - "message": "KDF-instellingen wijzigen" - }, "secureYourInfrastructure": { "message": "Beveilig je infrastructuur" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archief", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archiveren", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extensie geïnstalleerd!" }, + "openTheBitwardenExtension": { + "message": "De Bitwarden-extensie openen" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "De Bitwarden-extensie is geïnstalleerd! Open de extensie om in te loggen en te beginnen met automatisch invullen." + }, "openExtensionToAutofill": { "message": "Open de extensie om in te loggen en te beginnen met automatisch invullen." }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 54323d9b12a..92bfe3c925c 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Ugild hovudpassord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Det er inga oppføringar å lista opp." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 526b7567d99..173520dab61 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 8254d28f142..808981f5fb0 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Utwórz nowy element logowania" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Aplikacje krytyczne ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Powiadomieni członkowie ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Zagrożeni użytkownicy" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Hasło główne jest nieprawidłowe" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Hasło pliku jest nieprawidłowe. Użyj prawidłowego hasła." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Brak elementów." }, + "noItemsInTrash": { + "message": "Brak elementów w koszu" + }, + "noItemsInTrashDesc": { + "message": "Usunięte elementy pojawią się tutaj i zostaną trwale usunięte po 30 dniach" + }, + "noItemsInVault": { + "message": "Brak elementów w sejfie" + }, + "emptyVaultDescription": { + "message": "Sejf chroni nie tylko hasła. Przechowuj bezpiecznie dane logowania, identyfikatory, karty i notatki." + }, + "emptyFavorites": { + "message": "Brak ulubionych elementów" + }, + "emptyFavoritesDesc": { + "message": "Dodaj do ulubionych często używane elementy dla szybkiego dostępu." + }, + "noSearchResults": { + "message": "Brak pasujących elementów" + }, + "clearFiltersOrTryAnother": { + "message": "Wyczyść filtry lub użyj innej frazy" + }, "noPermissionToViewAllCollectionItems": { "message": "Nie masz uprawnień do przeglądania wszystkich elementów w tej kolekcji." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organizacja została zawieszona" }, + "organizationIsSuspended": { + "message": "Organizacja została zawieszona" + }, + "organizationIsSuspendedDesc": { + "message": "Nie można uzyskać dostępu do elementów w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." + }, "secretsAccessSuspended": { "message": "Nie można uzyskać dostępu do zawieszonych organizacji. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Konta serwisowe nie mogą być tworzone w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, - "disabledOrganizationFilterError": { - "message": "Nie można uzyskać dostępu do elementów w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." - }, "licenseIsExpired": { "message": "Licencja wygasła." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Identyfikator SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Podaj ten identyfikator swoim członkowm, aby zalogować się za pomocą SSO. Aby pominąć ten krok, ustaw ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Dowiedz się więcej", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Odłącz logowanie jednokrotne SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Odmowa dostępu. Nie masz uprawnień do przeglądania tej strony." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Grupa/Użytkownik" }, - "lowKdfIterations": { - "message": "Niska liczba iteracji KDF" - }, - "updateLowKdfIterationsDesc": { - "message": "Zaktualizuj ustawienia szyfrowania, aby spełnić nowe zalecenia bezpieczeństwa i poprawić ochronę konta." - }, "kdfSettingsChangeLogoutWarning": { "message": "Kontynuowanie spowoduje wylogowanie ze wszystkich aktywnych sesji. Będzie trzeba zalogować się ponownie i wykonać logowanie dwuetapowe, jeśli jest włączone. Zalecamy wyeksportowanie sejfu przed zmianą ustawień szyfrowania, aby zapobiec utracie danych." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Przypisz" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Przypisz do kolekcji" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Niska liczba iteracji KDF. Zwiększ liczbę iteracji, aby zwiększyć bezpieczeństwo Twojego konta." - }, - "changeKDFSettings": { - "message": "Zmień ustawienia KDF" - }, "secureYourInfrastructure": { "message": "Zabezpiecz swoją infrastrukturę" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Rozszerzenie Bitwarden zainstalowane!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Otwórz rozszerzenie, aby zalogować się i rozpocząć autouzupełnianie." }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index b2a2924b28b..5df0689d9de 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Criar item de \"login\"" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Aplicativos críticos ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Membros notificados ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Membros de risco" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Senha mestra inválida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Senha do arquivo inválida, por favor informe a senha utilizada quando criou o arquivo de exportação." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Não há itens para listar." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "Você não tem permissão para visualizar todos os itens desta coleção." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organização está desabilitada." }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "As organizações suspensas não podem ser acessadas. Entre em contato com o proprietário da organização para obter assistência." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Contas de serviço não podem ser criadas em organizações suspensas. Entre em contato com o proprietário da organização para obter assistência." }, - "disabledOrganizationFilterError": { - "message": "Itens em Organizações Desativadas não podem ser acessados. Entre em contato com o proprietário da sua Organização para obter ajuda." - }, "licenseIsExpired": { "message": "A licença está expirada." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Identificador SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Forneça esse ID aos seus membros para logar com SSO. Para ignorar essa etapa, configure ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Desvincular SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Acesso negado. Você não tem permissão para ver esta página." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Grupo/Usuário" }, - "lowKdfIterations": { - "message": "Iterações KDF baixas" - }, - "updateLowKdfIterationsDesc": { - "message": "Atualize suas configurações de criptografia para atender às novas recomendações de segurança e melhorar a proteção da conta." - }, "kdfSettingsChangeLogoutWarning": { "message": "O processo desconectará você de todas as sessões ativas. Você precisará iniciar a sessão novamente e concluir o login em duas etapas, se houver. Recomendamos exportar seu cofre antes de alterar suas configurações de criptografia para evitar perda de dados." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Atribuir" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Atribuir à coleções" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Iterações baixas do KDF. Aumente as suas iterações para melhorar a segurança da sua conta." - }, - "changeKDFSettings": { - "message": "Atualizar as definições do KDF" - }, "secureYourInfrastructure": { "message": "Proteja sua infraestrutura" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 6f8e4d9c11c..a7be84317e3 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -59,8 +59,61 @@ "createNewLoginItem": { "message": "Criar nova credencial" }, - "criticalApplicationsActivityDescription": { - "message": "Depois de marcar as aplicações como críticas, estas serão apresentadas aqui." + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "Once you mark applications critical, they will display here." + }, + "viewAtRiskMembers": { + "message": "Ver membros em risco" + }, + "viewAtRiskApplications": { + "message": "Ver aplicações em risco" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ de $TOTAL$ aplicações críticas estão em risco devido a palavras-passe em risco", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } }, "criticalApplicationsWithCount": { "message": "Aplicações críticas ($COUNT$)", @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ aplicações em risco", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Membros notificados ($COUNT$)", "placeholders": { @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "Membros em risco" }, - "membersAtRiskActivityDescription": { - "message": "Membros com acesso de edição a itens em risco para aplicações críticas" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "$COUNT$ membros em risco", "placeholders": { "count": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Palavra-passe mestra inválida" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Palavra-passe mestra inválida. Confirme se o seu e-mail está correto e se a sua conta foi criada em $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Palavra-passe de ficheiro inválida, utilize a palavra-passe que introduziu quando criou o ficheiro de exportação." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Não existem itens para listar." }, + "noItemsInTrash": { + "message": "Nenhum item no lixo" + }, + "noItemsInTrashDesc": { + "message": "Os itens que eliminar aparecerão aqui e serão permanentemente eliminados após 30 dias" + }, + "noItemsInVault": { + "message": "Nenhum item no cofre" + }, + "emptyVaultDescription": { + "message": "O cofre protege mais do que apenas as suas palavras-passe. Guarde aqui credenciais, IDs, cartões e notas de forma segura." + }, + "emptyFavorites": { + "message": "Não adicionou nenhum item aos favoritos" + }, + "emptyFavoritesDesc": { + "message": "Adicione itens utilizados frequentemente aos favoritos para um acesso rápido." + }, + "noSearchResults": { + "message": "Não foram apresentados resultados de pesquisa" + }, + "clearFiltersOrTryAnother": { + "message": "Limpe os filtros ou tente outro termo de pesquisa" + }, "noPermissionToViewAllCollectionItems": { "message": "Não tem permissão para ver todos os itens desta coleção." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organização suspensa" }, + "organizationIsSuspended": { + "message": "A organização está suspensa" + }, + "organizationIsSuspendedDesc": { + "message": "Não é possível aceder aos itens de organizações suspensas. Contacte o proprietário da organização para obter assistência." + }, "secretsAccessSuspended": { "message": "Não é possível aceder a organizações suspensas. Por favor, contacte o proprietário da organização para obter assistência." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "As contas de serviço não podem ser criadas em organizações suspensas. Por favor, contacte o proprietário da organização para obter assistência." }, - "disabledOrganizationFilterError": { - "message": "Não é possível aceder aos itens de organizações suspensas. Contacte o proprietário da organização para obter assistência." - }, "licenseIsExpired": { "message": "A licença expirou." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Identificador SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Forneça este ID aos seus membros para iniciarem sessão com o SSO. Para ignorar este passo, configure a ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Forneça este ID aos seus membros para iniciarem sessão com o SSO. Os membros podem ignorar a introdução deste identificador durante o SSO se estiver configurado um domínio reivindicado. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Saiba mais", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Desvincular SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Apenas o cofre da organização associado a $ORGANIZATION$ será exportado.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Apenas o cofre da organização associado a $ORGANIZATION$ será exportado. As coleções dos meus itens não serão incluídas.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Acesso negado. Não tem permissão para ver esta página." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Segredo desconhecido, poderá ser necessário pedir autorização para aceder a este segredo." }, + "unknownServiceAccount": { + "message": "Conta automática desconhecida, poderá precisar de pedir autorização para aceder a esta conta automática." + }, "unknownProject": { "message": "Projeto desconhecido, poderá ser necessário pedir autorização para aceder a este projeto." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Grupo/Utilizador" }, - "lowKdfIterations": { - "message": "Iterações KDF baixas" - }, - "updateLowKdfIterationsDesc": { - "message": "Atualize as suas definições de encriptação para cumprir as novas recomendações de segurança e melhorar a proteção da conta." - }, "kdfSettingsChangeLogoutWarning": { "message": "Ao prosseguir, sairá de todas as sessões ativas. Terá de voltar a iniciar sessão e concluir a verificação de dois passos, caso exista. Recomendamos que exporte o seu cofre antes de alterar as definições de encriptação para evitar a perda de dados." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Acedeu a um projeto com o ID: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Acedeu a um projeto com o identificador: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "ID da conta automática eliminada: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Editou um projeto com o identificador: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Utilizador adicionado: $USER_ID$ à conta automática com o identificador: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Utilizador removido: $USER_ID$ da conta automática com o identificador: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Grupo removido: $GROUP_ID$ da conta automática com o identificador: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Conta automática criada com o identificador: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Grupo adicionado: $GROUP_ID$ à conta automática com o identificador: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Conta automática eliminada com o identificador: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Eliminou um projeto com o identificador: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Atribuir" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Atribuir às coleções" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Falha ao guardar a integração. Por favor, tente novamente mais tarde." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Precisa de ser o proprietário da organização para executar esta ação." + }, "failedToDeleteIntegration": { "message": "Falha ao eliminar a integração. Por favor, tente novamente mais tarde." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Baixa quantidade de iterações do KDF. Aumente as suas iterações para melhorar a segurança da sua conta." - }, - "changeKDFSettings": { - "message": "Alterar as definições do KDF" - }, "secureYourInfrastructure": { "message": "Proteja a sua infraestrutura" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Procurar no arquivo" }, - "archive": { - "message": "Arquivar" + "archiveNoun": { + "message": "Arquivo", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arquivar", + "description": "Verb" }, "noItemsInArchive": { "message": "Nenhum item no arquivo" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Extensão Bitwarden instalada!" }, + "openTheBitwardenExtension": { + "message": "Abrir a extensão Bitwarden" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "A extensão Bitwarden está instalada! Abra a extensão para iniciar sessão e começar o preenchimento automático." + }, "openExtensionToAutofill": { "message": "Abra a extensão para iniciar sessão e começar o preenchimento automático." }, diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 364e7f86e64..3296b1a3a0a 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Parolă principală incorectă" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Parola fișierului nu este validă, utilizați parola introdusă atunci când ați creat fișierul de export." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Niciun articol de afișat." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organizație suspendată" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Articolele din organizațiile suspendate nu pot fi accesate. Contactați proprietarul organizației pentru asistență." - }, "licenseIsExpired": { "message": "Licența a expirat." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Deconectare SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Acces refuzat. Nu aveți permisiunea de a vizualiza această pagină." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 753b5745f36..8db4da0c6ca 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Создать новый логин" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "Завершено: $PERCENT$%", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "Завершено $COUNT$ из $TOTAL$ задач безопасности", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Прогресс смены пароля" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Критичные приложения ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Уведомленные участники ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Участники группы риска" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Неверный мастер-пароль" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Неверный мастер-пароль. Подтвердите, что ваш адрес email указан верно и ваш аккаунт был создан на $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Неверный пароль к файлу. Используйте пароль, введенный при создании файла экспорта." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Нет элементов для отображения." }, + "noItemsInTrash": { + "message": "Нет элементов в корзине" + }, + "noItemsInTrashDesc": { + "message": "Элементы, которые вы удаляете, появятся здесь и будут удалены навсегда через 30 дней" + }, + "noItemsInVault": { + "message": "В хранилище нет элементов" + }, + "emptyVaultDescription": { + "message": "Хранилище защищает не только ваши пароли. Логины, идентификаторы, карты и заметки в нем надежно защищены." + }, + "emptyFavorites": { + "message": "В избранном нет элементов" + }, + "emptyFavoritesDesc": { + "message": "Добавляйте часто используемые элементы в избранное для быстрого доступа." + }, + "noSearchResults": { + "message": "Поиск не дал результатов" + }, + "clearFiltersOrTryAnother": { + "message": "Очистите фильтры или попробуйте другой поисковый запрос" + }, "noPermissionToViewAllCollectionItems": { "message": "У вас нет разрешения на просмотр всех элементов этой коллекции." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Организация приостановлена" }, + "organizationIsSuspended": { + "message": "Организация приостановлена" + }, + "organizationIsSuspendedDesc": { + "message": "Доступ к элементам в отключенных организациях невозможен. Обратитесь за помощью к владельцу организации." + }, "secretsAccessSuspended": { "message": "Доступ к отключенным организациям невозможен. Обратитесь за помощью к владельцу организации." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Сервисные аккаунты не могут быть созданы в отключенных организациях. Обратитесь за помощью к владельцу организации." }, - "disabledOrganizationFilterError": { - "message": "Доступ к элементам в отключенных организациях невозможен. Обратитесь за помощью к владельцу организации." - }, "licenseIsExpired": { "message": "Срок действия лицензии истек." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO идентификатор" }, - "ssoIdentifierHintPartOne": { - "message": "Предоставьте этот ID пользователям для авторизации с помощью SSO. Чтобы обойти этот шаг, настройте ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Предоставьте этот ID своим пользователям для авторизации при помощи единого входа. Пользователи могут не вводить этот идентификатор во время единого входа, если настроен заявленный домен. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Подробнее", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Отключить SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Будет экспортировано только хранилище организации, связанное с $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Будет экспортировано только хранилище организации, связанное с $ORGANIZATION$. Коллекции Мои элементы включены не будут.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Доступ запрещен. У вас нет разрешения на просмотр этой страницы." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Неизвестный секрет. Вам может потребоваться запросить разрешение на доступ к этому секрету." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Неизвестный проект. Вам может потребоваться запросить разрешение на доступ к этому проекту." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Группа/Пользователь" }, - "lowKdfIterations": { - "message": "Низкие итерации KDF" - }, - "updateLowKdfIterationsDesc": { - "message": "Обновите настройки шифрования в соответствии с новыми рекомендациями по безопасности и улучшите защиту аккаунта." - }, "kdfSettingsChangeLogoutWarning": { "message": "При продолжении все активные сессии будут завершены. Вам потребуется авторизоваться повторно и выполнить двухэтапную аутентификацию, если она включена. Мы рекомендуем экспортировать хранилище перед изменением настроек шифрования, чтобы предотвратить потерю данных." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Получен доступ к проекту с ID: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Изменен проект с идентификатором: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Удален проект с идентификатором: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Назначить" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Назначить коллекциям" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Не удалось сохранить интеграцию. Пожалуйста, повторите попытку позже." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Для выполнения этого действия вы должны быть владельцем организации." + }, "failedToDeleteIntegration": { "message": "Не удалось удалить интеграцию. Пожалуйста, повторите попытку позже." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Низкое количество итераций KDF. Увеличьте количество итераций, чтобы повысить защищенность вашего аккаунта." - }, - "changeKDFSettings": { - "message": "Изменить параметры KDF" - }, "secureYourInfrastructure": { "message": "Защитите вашу инфраструктуру" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Поиск в архиве" }, - "archive": { - "message": "Архив" + "archiveNoun": { + "message": "Архив", + "description": "Noun" + }, + "archiveVerb": { + "message": "Архивировать", + "description": "Verb" }, "noItemsInArchive": { "message": "В архиве нет элементов" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Расширение Bitwarden установлено!" }, + "openTheBitwardenExtension": { + "message": "Открыть расширение Bitwarden" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "Расширение Bitwarden установлено! Откройте расширение, чтобы авторизоваться и начать автозаполнение." + }, "openExtensionToAutofill": { "message": "Откройте расширение, чтобы авторизоваться и начать использовать автозаполнение." }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index b2b9714e766..8db4f703b7e 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 396d37e157e..5bea697ab57 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -59,8 +59,61 @@ "createNewLoginItem": { "message": "Pridať novu položku s prihlásením" }, - "criticalApplicationsActivityDescription": { - "message": "Tu sa zobrazia aplikácie, ktoré označíte za kritické." + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "Once you mark applications critical, they will display here." + }, + "viewAtRiskMembers": { + "message": "Zobraziť ohrozených členov" + }, + "viewAtRiskApplications": { + "message": "Zobraziť ohrozené aplikácie" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ z $TOTAL$ kritických aplikácii je ohrozených nebezpečnými heslami", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } }, "criticalApplicationsWithCount": { "message": "Kritické aplikácie ($COUNT$)", @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ aplikácií ohrozených", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "Ohrozených členov" }, - "membersAtRiskActivityDescription": { - "message": "Členovia s oprávnením upravovať ohrozené položky kritických aplikácii" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "$COUNT$ ohrozených členov", "placeholders": { "count": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Neplatné hlavné heslo" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Neplatné heslo súboru, použite heslo, ktoré ste zadali pri vytváraní exportného súboru." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Neexistujú žiadne položky na zobrazenie." }, + "noItemsInTrash": { + "message": "V koši nie sú žiadne položky" + }, + "noItemsInTrashDesc": { + "message": "Položky, ktoré odstránite, sa zobrazia tu a po 30 dňoch sa odstránia natrvalo" + }, + "noItemsInVault": { + "message": "V trezore nie sú žiadne položky" + }, + "emptyVaultDescription": { + "message": "Trezor chráni viac ako len heslá. Môžete tu bezpečne ukladať prihlasovacie údaje, identifikačné údaje, karty a poznámky." + }, + "emptyFavorites": { + "message": "Neoznačili ste žiadnu položku za obľúbenú" + }, + "emptyFavoritesDesc": { + "message": "Pre rýchly prístup, pridajte položky medzi obľúbené." + }, + "noSearchResults": { + "message": "Nenašli sa žiadne výsledky vyhľadávania" + }, + "clearFiltersOrTryAnother": { + "message": "Vymažte filtre alebo zmeňte vyhľadávaný výraz" + }, "noPermissionToViewAllCollectionItems": { "message": "Nemáte povolenie pre zobrazenie všetkých položiek v tejto zbierke." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organizácia je vypnutá." }, + "organizationIsSuspended": { + "message": "Organizácia je pozastavená" + }, + "organizationIsSuspendedDesc": { + "message": "K položkám v pozastavenej organizácii nie je možné pristupovať. Požiadajte o pomoc vlastníka organizácie." + }, "secretsAccessSuspended": { "message": "K pozastaveným organizáciám nie je možné pristupovať. Požiadajte o pomoc vlastníka organizácie." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "V pozastavených organizáciám nie je možné vytvárať služobné kontá. Požiadajte o pomoc vlastníka organizácie." }, - "disabledOrganizationFilterError": { - "message": "K položkám vo vypnutej organizácii nie je možné pristupovať. Požiadajte o pomoc vlastníka organizácie." - }, "licenseIsExpired": { "message": "Licencia vypršala." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifikátor" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Zistiť viac", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Odpojiť SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Prístup zamietnutý. Nemáte oprávnenie na zobrazenie tejto stránky." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Neznáma položka možno potrebujete požiadať o prístup k tejto položke." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Neznámy projekt, možno potrebujete požiadať o prístup k tomuto projektu." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Skupina/Používateľ" }, - "lowKdfIterations": { - "message": "Nízky počet iterácií KDF" - }, - "updateLowKdfIterationsDesc": { - "message": "Aktualizujte vaše nastavenie šifrovania aby ste boli v súlade s bezpečnostnými odporúčaniami a vylepšili si tak ochranu vášho konta." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Pristúp k projektu s identifikátorom: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Upravený projekt s identifikátorom: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Odstránený projekt s identifikátorom: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Prideliť" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Prideliť k zbierkam" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Nepodarilo sa uložiť integráciu. Prosím skúste to neskôr." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Pre vykonanie tejto akcie musíte by vlastník organizácie." + }, "failedToDeleteIntegration": { "message": "Nepodarilo sa odstrániť integráciu. Prosím skúste to neskôr." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Nízky počet iterácií KDF. Pre zlepšenie bezpečnosti vášho konta, zvýšte počet iterácií." - }, - "changeKDFSettings": { - "message": "Zmeniť nastavenia KDF" - }, "secureYourInfrastructure": { "message": "Zabezpečte si infraštruktúru" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Prehľadať archív" }, - "archive": { - "message": "Archív" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "Žiadne položky v archíve" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Rozšírenie Bitwarden nainštalované!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Otvorte rozšírenie, prihláste sa a začnite automatické vypĺňanie." }, diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index e54ef8bb4b6..f0acd2ca34b 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Napačno glavno geslo" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Napačno geslo za datoteko. Prosimo, vnesite geslo, ki ste ga vnesli, ko ste izvozili datoteko." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Ni elementov za prikaz." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "Nimate dovoljenja za ogled vseh elementov v tej zbirki." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "Licenca je potekla" }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Nizko število ponovitev KDF" - }, - "updateLowKdfIterationsDesc": { - "message": "Popravite svoje nastavitve šifriranja, da bodo v skladu z novimi priporočili za varnost, in izboljšajte zaščito svojega računa." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 8b8edaa1ec3..1eaf8f25274 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Kreirajte novu stavku za prijavu" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Kritične aplikacije ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Obavešteni članovi ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Nema stavki u listi." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index 68a440646a3..e31c138121f 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Креирајте нову ставку за пријаву" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% завршено", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "Завршени сигурносни задаци: $COUNT$ од $TOTAL$", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Напредак промене лозинке" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Доделите задатке чланова да прате напредак" + }, + "onceYouReviewApplications": { + "message": "Једном када прегледате апликације и означите их као критичне, оне ће се овде приказати." + }, + "sendReminders": { + "message": "Пошаљи подсетнике" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Апликације обележене као критичне, су приказане овде." }, + "viewAtRiskMembers": { + "message": "Преглед чланова под ризиком" + }, + "viewAtRiskApplications": { + "message": "Преглед апликација под ризиком" + }, + "criticalApplicationsAreAtRisk": { + "message": "Критичне апликације ризиковне због ризичнр лозинке: $COUNT$ од $TOTAL$", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Критичне апликације ($COUNT$)", "placeholders": { @@ -72,7 +125,25 @@ } }, "countOfCriticalApplications": { - "message": "$COUNT$ critical applications", + "message": "Критичне апликације: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfApplicationsAtRisk": { + "message": "Апликације у ризику: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "Лозинке у ризику: $COUNT$", "placeholders": { "count": { "content": "$1", @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "Чланови под ризиком" }, - "membersAtRiskActivityDescription": { - "message": "Чланови са правом уређивања за угрожене ставке критичних апликација" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Чланови са приступом за угрожене ставке критичних апликација" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "Угрожени чланови: $COUNT$", "placeholders": { "count": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Погрешна главна лозинка" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Неважећа главна лозинка. Потврдите да је ваш имејл тачан и ваш рачун је креиран на $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Неважећа лозинка за датотеку, користите лозинку коју сте унели када сте креирали датотеку за извоз." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Нама ставке у листи." }, + "noItemsInTrash": { + "message": "Нема ставки у смећу" + }, + "noItemsInTrashDesc": { + "message": "Ставке које избришете ће се појавити овде и биће трајно избрисане након 30 дана" + }, + "noItemsInVault": { + "message": "Сеф је празан" + }, + "emptyVaultDescription": { + "message": "Сеф штити више од само ваших лозинки. Овде безбедно чувајте безбедне пријаве, личне карте, картице и белешке." + }, + "emptyFavorites": { + "message": "Нисте фаворизовали никакве ставке" + }, + "emptyFavoritesDesc": { + "message": "Додајте често кориштене ставке у фаворите за брз приступ." + }, + "noSearchResults": { + "message": "Нема резултата претраге" + }, + "clearFiltersOrTryAnother": { + "message": "Обришите филтере или покушајте са другим термином" + }, "noPermissionToViewAllCollectionItems": { "message": "Немате дозволу да видите све ставке у овој колекцији." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Организација је онемогућена." }, + "organizationIsSuspended": { + "message": "Организација је онемогућена" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Суспендованим организацијама се не може приступити. За помоћ контактирајте власника своје организације." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Сервисни налози се не могу креирати у суспендованим организацијама. За помоћ контактирајте власника своје организације." }, - "disabledOrganizationFilterError": { - "message": "Није могуће приступити ставкама у онемогућене организације. Обратите се власнику организације за помоћ." - }, "licenseIsExpired": { "message": "Лиценца је истекла." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO идентификација" }, - "ssoIdentifierHintPartOne": { - "message": "Дајте овај ИД својим члановима да се пријаве са ССО. Да бисте заобишли овај корак, подесите ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Сазнај више", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Откачи SSO" @@ -6570,7 +6681,7 @@ "message": "You've been offered a free Bitwarden Families plan organization. To continue, you need to log in to the account that received the offer." }, "sponsoredFamiliesAcceptFailed": { - "message": "Unable to accept offer. Please resend the offer email from your Enterprise account and try again." + "message": "Није могуће прихватити понуду. Пошаљите имејл са свог Enterprise налога и покушајте поново." }, "sponsoredFamiliesAcceptFailedShort": { "message": "Није могуће прихватити понуду. $DESCRIPTION$", @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Одбијен приступ. Немате дозволу да видите ову страницу." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -7389,7 +7521,7 @@ "message": "Променити кључ" }, "scimApiKey": { - "message": "SCIM API key", + "message": "SCIM API кључ", "description": "the text, 'SCIM' and 'API', are acronyms and should not be translated." }, "copyScimUrl": { @@ -7433,7 +7565,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Следећи знакови нису дозвољени: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -7499,10 +7631,10 @@ "message": "DUO пријава у два корака је потребна за ваш налог." }, "duoTwoFactorRequiredPageSubtitle": { - "message": "Duo two-step login is required for your account. Follow the steps below to finish logging in." + "message": "За ваш налог је потребан два корака. Следите наведене кораке да бисте завршили пријављивање." }, "followTheStepsBelowToFinishLoggingIn": { - "message": "Follow the steps below to finish logging in." + "message": "Следите наведене кораке да бисте завршили пријављивање." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { "message": "Следите наведене кораке да бисте завршили пријаву са својим безбедносним кључем." @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Група/Корисник" }, - "lowKdfIterations": { - "message": "Ниска KDF понављања" - }, - "updateLowKdfIterationsDesc": { - "message": "Ажурирајте подешавања шифровања да бисте испунили нове безбедносне препоруке и побољшали заштиту налога." - }, "kdfSettingsChangeLogoutWarning": { "message": "Ако наставите, одјавићете се са свих активних сесија. Мораћете поново да се пријавите и завршитепријаве у два корака, ако имате. Препоручујемо да извезете трезор пре него што промените подешавања шифровања да бисте спречили губитак података." }, @@ -8389,7 +8515,7 @@ "message": "Менаџер тајни" }, "secretsManagerAccessDescription": { - "message": "Activate user access to Secrets Manager." + "message": "Активирај приступ кориснику Secrets Manager-у." }, "userAccessSecretsManagerGA": { "message": "Овај корисник може приступити Менаџеру Тајни" @@ -8459,7 +8585,7 @@ } }, "permanentlyDeletedSecretWithId": { - "message": "Permanently deleted a secret with identifier: $SECRET_ID$", + "message": "Тајна трајно избрисана са идентификатором: $SECRET_ID$", "placeholders": { "secret_id": { "content": "$1", @@ -8468,7 +8594,7 @@ } }, "restoredSecretWithId": { - "message": "Restored a secret with identifier: $SECRET_ID$", + "message": "Тајна враћена са идентификатором: $SECRET_ID$", "placeholders": { "secret_id": { "content": "$1", @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Приступљен пројекат са ИД: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Уређен пројекат са ИД: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Избрисан пројекат са ИД: $PROJECT_ID$", "placeholders": { @@ -8941,7 +9146,7 @@ "message": "Недостаје имејл корисника" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Имејл активног корисника није пронађен. Одјављивање." }, "deviceTrusted": { "message": "Уређај поуздан" @@ -8950,7 +9155,7 @@ "message": "Позови кориснике" }, "secretsManagerForPlan": { - "message": "Secrets Manager for $PLAN$", + "message": "Secrets Manager за $PLAN$", "placeholders": { "plan": { "content": "$1", @@ -9357,6 +9562,9 @@ "assign": { "message": "Додели" }, + "assignTasks": { + "message": "Додели задатке" + }, "assignToCollections": { "message": "Додели колекцијама" }, @@ -9364,10 +9572,10 @@ "message": "Додели овим колекцијама" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Only organization members with access to these collections will be able to see the item." + "message": "Само чланови организације са приступом овим збиркама ће моћи да виде ставку." }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "Само чланови организације са приступом овим збиркама ће моћи да виде ставке." }, "selectCollectionsToAssign": { "message": "Изаберите колекције за доделу" @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Није успело сачувавање интеграције. Покушајте поново касније." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9979,7 +10190,7 @@ "message": "Управљени провајдери сервиса" }, "managedServiceProvider": { - "message": "Managed service provider" + "message": "Управљачки провајдер сервиса" }, "multiOrganizationEnterprise": { "message": "Multi-organization enterprise" @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Ниске KDF итерације. Повећајте број понављања да бисте побољшали безбедност свог налога." - }, - "changeKDFSettings": { - "message": "Променити KDF подешавања" - }, "secureYourInfrastructure": { "message": "Обезбедите своју инфраструктуру" }, @@ -10681,16 +10886,16 @@ "message": "Ваш рачун је пријављен на сваку од доле наведених уређаја." }, "claimedDomains": { - "message": "Claimed domains" + "message": "Захтевани домене" }, "claimDomain": { - "message": "Claim domain" + "message": "Захтевај домен" }, "reclaimDomain": { - "message": "Reclaim domain" + "message": "Поновно уахтевање домена" }, "claimDomainNameInputHint": { - "message": "Example: mydomain.com. Subdomains require separate entries to be claimed." + "message": "Пример: mydomain.com. Под-домени захтевају посебне уносе да би били захтевани." }, "automaticClaimedDomains": { "message": "Automatic Claimed Domains" @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Претражи архиву" }, - "archive": { - "message": "Архива" + "archiveNoun": { + "message": "Архива", + "description": "Noun" + }, + "archiveVerb": { + "message": "Архива", + "description": "Verb" }, "noItemsInArchive": { "message": "Нема ставка у архиви" @@ -11037,10 +11247,10 @@ "message": "Пословна јединица" }, "businessUnits": { - "message": "Business Units" + "message": "Пословне јединице" }, "newBusinessUnit": { - "message": "New business unit" + "message": "Нова пословна јединица" }, "sendsTitleNoItems": { "message": "Шаљите бзбедно осетљиве информације", @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden екстензија инсталираана!" }, + "openTheBitwardenExtension": { + "message": "Отворити Bitwarden екстензију" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Отворите екстензију да бисте се пријавили и започели ауто-попуњавање." }, @@ -11188,10 +11404,10 @@ "description": "Error message shown when trying to add credit to a trialing organization without a billing address." }, "aboutThisSetting": { - "message": "About this setting" + "message": "О овом подешавању" }, "permitCipherDetailsDescription": { - "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + "message": "Bitwarden ће користити сачуване URI-јеве за пријаву да би одредио коју икону или URL за промену лозинке треба користити како би побољшао ваше искуство. Никакви подаци нису сакупљени нити сачувани приликом коришћења ове услуге." }, "billingAddress": { "message": "Адреса рачуна" @@ -11293,7 +11509,7 @@ "message": "Неограничене тајне и пројекте" }, "providersubscriptionCanceled": { - "message": "Subscription canceled" + "message": "Претплата је отказана" }, "providersubCanceledmessage": { "message": "To resubscribe, contact Bitwarden Customer Support." @@ -11406,13 +11622,13 @@ "message": "Advanced capabilities for any organization" }, "planNameCustom": { - "message": "Custom plan" + "message": "Прилагођени план" }, "planDescCustom": { "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." }, "builtInAuthenticator": { - "message": "Built-in authenticator" + "message": "Уграђени аутентификатор" }, "breachMonitoring": { "message": "Праћење повreda безбедности" @@ -11427,7 +11643,7 @@ "message": "Неограничено дељење – изаберите ко шта може да види" }, "familiesUnlimitedCollections": { - "message": "Unlimited family collections" + "message": "Неограничене фамилија колекције" }, "familiesSharedStorage": { "message": "Заједничко складиштење важних породичних информација" diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 853e60c95bc..9523343079c 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Skapa nytt inloggningsobjekt" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% färdigt", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ av $TOTAL$ säkerhetsuppgifter slutförda", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Skicka påminnelser" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "Visa medlemmar i riskzonen" + }, + "viewAtRiskApplications": { + "message": "Visa applikationer i riskzonen" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ av $TOTAL$ kritiska applikationer är i riskzonen på grund av lösenord i riskzonen", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Kritiska applikationer ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applikationer i riskzonen", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Meddelade medlemmar ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Riskutsatta medlemmar" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ medlemmar i riskzonen", "placeholders": { "count": { "content": "$1", @@ -782,11 +853,11 @@ "description": "Header for new SSH key item type" }, "newItemHeaderTextSend": { - "message": "New Text Send", + "message": "Ny textsändning", "description": "Header for new text send" }, "newItemHeaderFileSend": { - "message": "New File Send", + "message": "Ny filsändning", "description": "Header for new file send" }, "editItemHeaderLogin": { @@ -810,11 +881,11 @@ "description": "Header for edit SSH key item type" }, "editItemHeaderTextSend": { - "message": "Edit Text Send", + "message": "Redigera textsändning", "description": "Header for edit text send" }, "editItemHeaderFileSend": { - "message": "Edit File Send", + "message": "Redigera filsändning", "description": "Header for edit file send" }, "viewItemHeaderLogin": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Ogiltigt huvudlösenord" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ogiltigt huvudlösenord. Bekräfta att din e-postadress är korrekt och ditt konto skapades på $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Ogiltigt fillösenord, använd lösenordet du angav när du skapade exportfilen." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Det finns inga objekt att visa." }, + "noItemsInTrash": { + "message": "Inga objekt i papperskorgen" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "Inga objekt i valvet" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "Du har inga favoritobjekt" + }, + "emptyFavoritesDesc": { + "message": "Lägg till ofta använda objekt till favoriter för snabb åtkomst." + }, + "noSearchResults": { + "message": "Inga sökresultat returnerades" + }, + "clearFiltersOrTryAnother": { + "message": "Töm filter eller försök med en annan sökterm" + }, "noPermissionToViewAllCollectionItems": { "message": "Du har inte behörighet att se alla objekt i denna samling." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organisationen är inaktiverad" }, + "organizationIsSuspended": { + "message": "Organisationen är avstängd" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Avstängda organisationer kan inte nås. Kontakta din organisationsägare för hjälp." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Servicekonton kan inte skapas i avstängda organisationer. Kontakta din organisationsägare för att få hjälp." }, - "disabledOrganizationFilterError": { - "message": "Det går inte att komma åt objekt i avstängda organisationer. Kontakta din organisationsägare för hjälp." - }, "licenseIsExpired": { "message": "Licensen har upphört att gälla." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO-identifierare" }, - "ssoIdentifierHintPartOne": { - "message": "Ge detta ID till dina medlemmar så att de kan logga in med SSO. För att kringgå detta steg, konfigurera ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Läs mer", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Avlänka SSO" @@ -6729,7 +6840,7 @@ "message": "SSO inaktiverad" }, "emailMustLoginWithSso": { - "message": "$EMAIL$ must login with Single Sign-on", + "message": "$EMAIL$ måste logga in med Single Sign-on", "placeholders": { "email": { "content": "$1", @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Åtkomst nekad. Du har inte behörighet att visa den här sidan." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Okänd hemlighet, du kan behöver begära behörighet för att komma åt denna hemlighet." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Okänt projekt, du kan behöver begära behörighet för att komma åt detta projekt." }, @@ -7671,11 +7803,11 @@ "description": "Label for a secret (key/value pair)" }, "serviceAccount": { - "message": "Tjänstkonto", + "message": "Tjänstekonto", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "serviceAccounts": { - "message": "Tjänstkonton", + "message": "Tjänstekonton", "description": "The title for the section that deals with service accounts." }, "secrets": { @@ -7699,7 +7831,7 @@ "description": "Title for creating a new secret." }, "newServiceAccount": { - "message": "Nytt tjänstkonto", + "message": "Nytt tjänstekonto", "description": "Title for creating a new service account." }, "secretsNoItemsTitle": { @@ -7714,7 +7846,7 @@ "message": "Det finns inga hemligheter i papperskorgen." }, "serviceAccountsNoItemsMessage": { - "message": "Skapa ett nytt tjänstkonto för att komma igång med automatisering av hemlig åtkomst.", + "message": "Skapa ett nytt tjänstekonto för att komma igång med automatisering av hemlig åtkomst.", "description": "Message to encourage the user to start creating service accounts." }, "serviceAccountsNoItemsTitle": { @@ -7726,19 +7858,19 @@ "description": "Placeholder text for searching secrets." }, "deleteServiceAccounts": { - "message": "Ta bort tjänstkonton", + "message": "Ta bort tjänstekonton", "description": "Title for the action to delete one or multiple service accounts." }, "deleteServiceAccount": { - "message": "Ta bort tjänstkonto", + "message": "Ta bort tjänstekonto", "description": "Title for the action to delete a single service account." }, "viewServiceAccount": { - "message": "Visa tjänstkonto", + "message": "Visa tjänstekonto", "description": "Action to view the details of a service account." }, "deleteServiceAccountDialogMessage": { - "message": "Att ta bort tjänstkontot $SERVICE_ACCOUNT$ är permanent och kan inte ångras.", + "message": "Att ta bort tjänstekontot $SERVICE_ACCOUNT$ är permanent och kan inte ångras.", "placeholders": { "service_account": { "content": "$1", @@ -7747,7 +7879,7 @@ } }, "deleteServiceAccountsDialogMessage": { - "message": "Att ta bort tjänstkonton är permanent och kan inte ångras." + "message": "Att ta bort tjänstekonton är permanent och kan inte ångras." }, "deleteServiceAccountsConfirmMessage": { "message": "Radera $COUNT$ tjänstkonton", @@ -7762,14 +7894,14 @@ "message": "Servicekonto raderat" }, "deleteServiceAccountsToast": { - "message": "Tjänstkonton togs bort" + "message": "Tjänstekonton togs bort" }, "searchServiceAccounts": { - "message": "Sök efter tjänstkonton", + "message": "Sök efter tjänstekonton", "description": "Placeholder text for searching service accounts." }, "editServiceAccount": { - "message": "Redigera tjänstkonto", + "message": "Redigera tjänstekonto", "description": "Title for editing a service account." }, "addProject": { @@ -7822,15 +7954,15 @@ "description": "" }, "serviceAccountName": { - "message": "Tjänstkontonamn", + "message": "Tjänstekontonamn", "description": "Label for the name of a service account" }, "serviceAccountCreated": { - "message": "Tjänstkonto skapat", + "message": "Tjänstekonto skapat", "description": "Notifies that a new service account has been created" }, "serviceAccountUpdated": { - "message": "Tjänstkonto uppdaterat", + "message": "Tjänstekonto uppdaterat", "description": "Notifies that a service account has been updated" }, "typeOrSelectProjects": { @@ -8359,13 +8491,13 @@ "message": "Lägg till personer eller grupper för att börja samarbeta" }, "projectEmptyServiceAccountAccessPolicies": { - "message": "Lägg till tjänstkonton för att bevilja åtkomst" + "message": "Lägg till tjänstekonton för att bevilja åtkomst" }, "serviceAccountPeopleDescription": { "message": "Ge grupper eller personer åtkomst till detta servicekonto." }, "serviceAccountProjectsDescription": { - "message": "Tilldela projekt till detta tjänstkonto. " + "message": "Tilldela projekt till detta tjänstekonto. " }, "serviceAccountEmptyProjectAccesspolicies": { "message": "Lägg till projekt för att bevilja åtkomst" @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Grupp/Användare" }, - "lowKdfIterations": { - "message": "Låga KDF-iterationer" - }, - "updateLowKdfIterationsDesc": { - "message": "Uppdatera dina krypteringsinställningar så att de uppfyller nya säkerhetsrekommendationer och förbättrar kontots skydd." - }, "kdfSettingsChangeLogoutWarning": { "message": "Om du fortsätter loggas du ut från alla aktiva sessioner. Du måste logga in igen och genomföra tvåstegsinloggning, om sådan finns. Vi rekommenderar att du exporterar ditt valv innan du ändrar dina krypteringsinställningar för att förhindra dataförlust." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Gick in i ett projekt med id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Redigerade ett projekt med identifieraren: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Tog bort ett projekt med identifieraren: $PROJECT_ID$", "placeholders": { @@ -8553,7 +8758,7 @@ "message": "Skapa ett projekt" }, "createServiceAccount": { - "message": "Skapa ett tjänstkonto" + "message": "Skapa ett tjänstekonto" }, "downloadThe": { "message": "Ladda ner", @@ -9357,6 +9562,9 @@ "assign": { "message": "Tilldela" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Tilldela till samlingar" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Misslyckades med att spara integration. Försök igen senare." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Du måste vara organisationens ägare för att utföra denna åtgärd." + }, "failedToDeleteIntegration": { "message": "Misslyckades med att ta bort integrationen. Försök igen senare." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Låga KDF-iterationer. Öka dina iterationer för att förbättra säkerheten för ditt konto." - }, - "changeKDFSettings": { - "message": "Ändra KDF-inställningar" - }, "secureYourInfrastructure": { "message": "Säkra din infrastruktur" }, @@ -11022,16 +11227,21 @@ "message": "Gratisorganisationer kan ha upp till 2 samlingar. Uppgradera till en betald plan för att lägga till fler samlingar." }, "searchArchive": { - "message": "Search archive" + "message": "Sök i arkiv" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Arkiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arkivera", + "description": "Verb" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Inga objekt i arkivet" }, "archivedItemsDescription": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Arkiverade objekt kommer att visas här och kommer att uteslutas från allmänna sökresultat och förslag för autofyll." }, "businessUnit": { "message": "Affärsenhet" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitvärdesförlängningen installerad!" }, + "openTheBitwardenExtension": { + "message": "Öppna Bitwarden-tillägget" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Öppna tillägget för att logga in och starta autofyllning." }, @@ -11370,7 +11586,7 @@ "message": "Additional storage GB" }, "additionalServiceAccountsV2": { - "message": "Additional machine accounts" + "message": "Ytterligare maskinkonton" }, "secretsManagerSeats": { "message": "Secrets Manager seats" @@ -11391,10 +11607,10 @@ "message": "Complete online security" }, "planDescFamiliesV2": { - "message": "Premium security for your family" + "message": "Premiumsäkerhet för din familj" }, "planDescFreeV2": { - "message": "Share with $COUNT$ other user", + "message": "Dela med $COUNT$ annan användare", "placeholders": { "count": { "content": "$1", @@ -11403,37 +11619,37 @@ } }, "planDescEnterpriseV2": { - "message": "Advanced capabilities for any organization" + "message": "Avancerade förmågor för alla organisationer" }, "planNameCustom": { - "message": "Custom plan" + "message": "Anpassad plan" }, "planDescCustom": { - "message": "Bitwarden scales with businesses of all sizes to secure passwords and sensitive information. If you're part of a large enterprise, contact sales to request a quote." + "message": "Bitwarden skalar med företag i alla storlekar för att säkra lösenord och känslig information. Om du är en del av ett stort företag, kontakta vår försäljningsavdelning för att begära en offert." }, "builtInAuthenticator": { - "message": "Built-in authenticator" + "message": "Inbyggd authenticator" }, "breachMonitoring": { - "message": "Breach monitoring" + "message": "Intrångsmonitorering" }, "andMoreFeatures": { "message": "Och mer!" }, "secureFileStorage": { - "message": "Secure file storage" + "message": "Säker fillagring" }, "familiesUnlimitedSharing": { - "message": "Unlimited sharing - choose who sees what" + "message": "Obegränsad delning - välj vem som ser vad" }, "familiesUnlimitedCollections": { - "message": "Unlimited family collections" + "message": "Obegränsade familjesamlingar" }, "familiesSharedStorage": { - "message": "Shared storage for important family info" + "message": "Delad lagring för viktig familjeinformation" }, "limitedUsersV2": { - "message": "Up to $COUNT$ members", + "message": "Upp till $COUNT$ medlemmar", "placeholders": { "count": { "content": "$1", @@ -11442,7 +11658,7 @@ } }, "limitedCollectionsV2": { - "message": "Up to $COUNT$ collections", + "message": "Upp till $COUNT$ samlingar", "placeholders": { "count": { "content": "$1", @@ -11451,13 +11667,13 @@ } }, "alwaysFree": { - "message": "Always free" + "message": "Alltid gratis" }, "twoSecretsIncluded": { - "message": "2 secrets" + "message": "2 hemligheter" }, "projectsIncludedV2": { - "message": "$COUNT$ project(s)", + "message": "$COUNT$ projekt", "placeholders": { "count": { "content": "$1", @@ -11466,13 +11682,13 @@ } }, "secureItemSharing": { - "message": "Secure item sharing" + "message": "Säker objektdelning" }, "scimSupport": { - "message": "SCIM support" + "message": "SCIM-stöd" }, "includedMachineAccountsV2": { - "message": "$COUNT$ machine accounts", + "message": "$COUNT$ maskinkonton", "placeholders": { "count": { "content": "$1", @@ -11481,21 +11697,21 @@ } }, "enterpriseSecurityPolicies": { - "message": "Enterprise security policies" + "message": "Säkerhetspolicyer för företag" }, "selfHostOption": { "message": "Self-host option" }, "complimentaryFamiliesPlan": { - "message": "Complimentary families plan for all users" + "message": "Kostnadsfri familjeplan för alla användare" }, "strengthenCybersecurity": { - "message": "Strengthen cybersecurity" + "message": "Förbättrad cybersäkerhet" }, "boostProductivity": { - "message": "Boost productivity" + "message": "Maximera produktiviteten" }, "seamlessIntegration": { - "message": "Seamless integration" + "message": "Sömlös integration" } } diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index dc3e32726d4..e2426c2d69b 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "புதிய உள்நுழைவு உருப்படியை உருவாக்கவும்" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "முக்கியமான பயன்பாடுகள் ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "அறிவிக்கப்பட்ட உறுப்பினர்கள் ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "ஆபத்தில் உள்ள உறுப்பினர்கள்" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "தவறான முதன்மை கடவுச்சொல்" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "தவறான கோப்பு கடவுச்சொல், ஏற்றுமதி கோப்பை உருவாக்கியபோது நீங்கள் உள்ளிட்ட கடவுச்சொல்லைப் பயன்படுத்தவும்." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "பட்டியலிட உருப்படிகள் எதுவும் இல்லை." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "இந்தத் தொகுப்பிலுள்ள எல்லா உருப்படிகளையும் பார்க்க உங்களுக்கு அனுமதி இல்லை." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "நிறுவனம் இடைநிறுத்தப்பட்டது" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "இடைநிறுத்தப்பட்ட நிறுவனங்களை அணுக முடியாது. உதவிக்கு உங்கள் நிறுவன உரிமையாளரைத் தொடர்புகொள்ளவும்." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "இடைநிறுத்தப்பட்ட நிறுவனங்களில் சேவை கணக்குகளை உருவாக்க முடியாது. உதவிக்கு உங்கள் நிறுவன உரிமையாளரைத் தொடர்புகொள்ளவும்." }, - "disabledOrganizationFilterError": { - "message": "இடைநிறுத்தப்பட்ட நிறுவனங்களில் உள்ள பொருட்களை அணுக முடியாது. உதவிக்கு உங்கள் நிறுவன உரிமையாளரைத் தொடர்புகொள்ளவும்." - }, "licenseIsExpired": { "message": "உரிமம் காலாவதியாகிவிட்டது." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO அடையாளங்காட்டி" }, - "ssoIdentifierHintPartOne": { - "message": "SSO உடன் உள்நுழைய இந்த ஐடியை உங்கள் உறுப்பினர்களுக்கு வழங்கவும். இந்த படியைத் தவிர்க்க, அமைக்கவும் ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "SSO இணைப்பை நீக்கு" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "அணுகல் மறுக்கப்பட்டது. இந்தப் பக்கத்தைப் பார்க்க உங்களுக்கு அனுமதி இல்லை." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "அறியப்படாத ரகசியம், இந்த ரகசியத்தை அணுகுவதற்கு நீங்கள் அனுமதி கோர வேண்டியிருக்கலாம்." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "அறியப்படாத திட்டம், இந்த திட்டத்தை அணுகுவதற்கு நீங்கள் அனுமதி கோர வேண்டியிருக்கலாம்." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "குழு/பயனர்" }, - "lowKdfIterations": { - "message": "குறைந்த KDF இட்டரேஷன்ஸ்" - }, - "updateLowKdfIterationsDesc": { - "message": "புதிய பாதுகாப்புப் பரிந்துரைகளை பூர்த்தி செய்யவும் மற்றும் கணக்கு பாதுகாப்பை மேம்படுத்தவும் உங்கள் குறியாக்க அமைப்புகளைப் புதுப்பிக்கவும்." - }, "kdfSettingsChangeLogoutWarning": { "message": "தொடர்வது அனைத்து செயலில் உள்ள அமர்வுகளிலிருந்தும் உங்களை வெளியேற்றும். நீங்கள் மீண்டும் உள்நுழைய வேண்டும் மற்றும் இரண்டு-படி உள்நுழைவை முடிக்க வேண்டும், ஏதேனும் இருந்தால். தரவு இழப்பைத் தடுக்க உங்கள் குறியாக்க அமைப்புகளை மாற்றுவதற்கு முன் உங்கள் பெட்டகத்தை ஏற்றுமதி செய்யுமாறு பரிந்துரைக்கிறோம்." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "திட்டம் Id: $PROJECT_ID$ அணுகப்பட்டது.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "ஒரு திட்டத்தை திருத்தப்பட்டது: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "ஒரு திட்டத்தை நீக்கப்பட்டது: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "ஒதுக்கு" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "சேகரிப்புகளுக்கு ஒதுக்கு" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "ஒருங்கிணைப்பைச் சேமிக்கத் தவறிவிட்டது. பின்னர் மீண்டும் முயற்சிக்கவும்." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "குறைந்த KDF இட்டரேஷன்கள். உங்கள் கணக்கின் பாதுகாப்பை மேம்படுத்த, உங்கள் இட்டரேஷன்களை அதிகரிக்கவும்." - }, - "changeKDFSettings": { - "message": "KDF அமைப்புகளை மாற்றவும்" - }, "secureYourInfrastructure": { "message": "உங்கள் கட்டமைப்பைப் பாதுகாக்கவும்" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden நீட்டிப்பு நிறுவப்பட்டது!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "உள்நுழைந்து தானாக நிரப்பத் தொடங்க நீட்டிப்பைத் திறக்கவும்." }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 526b7567d99..173520dab61 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Invalid master password" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "There are no items to list." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 0f9c3bea24d..9a8958651a2 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Create new login item" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Critical applications ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "At-risk members" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "รหัสผ่านหลักไม่ถูกต้อง" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "ไม่มีรายการการสำหรับแสดง" }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "You do not have permission to view all items in this collection." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Organization suspended" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Service accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, - "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." - }, "licenseIsExpired": { "message": "License is expired." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO identifier" }, - "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Unlink SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Group/User" }, - "lowKdfIterations": { - "message": "Low KDF Iterations" - }, - "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Assign" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Low KDF iterations. Increase your iterations to improve the security of your account." - }, - "changeKDFSettings": { - "message": "Change KDF settings" - }, "secureYourInfrastructure": { "message": "Secure your infrastructure" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index f36a336a4d1..6b19e0b5fe7 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -59,8 +59,61 @@ "createNewLoginItem": { "message": "Yeni hesap kaydı oluştur" }, - "criticalApplicationsActivityDescription": { - "message": "Uygulamaları kritik olarak işaretlediğinizde, bunlar burada görüntülenir." + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "Once you mark applications critical, they will display here." + }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } }, "criticalApplicationsWithCount": { "message": "Kritik uygulamalar ($COUNT$)", @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Bildirilen üyeler ($COUNT$)", "placeholders": { @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "Riskli üyeler" }, - "membersAtRiskActivityDescription": { - "message": "Kritik uygulamalar için risk altındaki kayıtlara düzenleme erişimi olan üyeler" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Kritik uygulamalar için risk altındaki kayıtlara erişimi olan üyeler" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "$COUNT$ üye risk altında", "placeholders": { "count": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Geçersiz ana parola" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Ana parola geçersiz. E-posta adresinizin doğru olduğunu ve hesabınızın $HOST$ üzerinde oluşturulduğunu kontrol edin.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Geçersiz dosya parolası. Lütfen dışa aktardığınız dosyayı oluştururken girdiğiniz parolayı kullanın." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Listelenecek kayıt yok." }, + "noItemsInTrash": { + "message": "Çöp kutusunda hiç kayıt yok" + }, + "noItemsInTrashDesc": { + "message": "Sildiğiniz kayıtlar burada görünecek ve 30 gün sonra kalıcı olarak silinecektir" + }, + "noItemsInVault": { + "message": "Kasada hiç kayıt yok" + }, + "emptyVaultDescription": { + "message": "Kasanız sadece parolalarınız için değil. Hesaplarınızı, kimliklerinizi, kredi kartlarınızı ve notlarınızı da güvenle burada depolayabilirsiniz." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Filtreleri temizleyin veya başka bir arama yapmayı deneyin" + }, "noPermissionToViewAllCollectionItems": { "message": "Bu koleksiyondaki tüm kayıtları görmek için izniniz yok." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Kuruluş askıya alındı" }, + "organizationIsSuspended": { + "message": "Kuruluş askıya alındı" + }, + "organizationIsSuspendedDesc": { + "message": "Askıya alınmış kuruluşlardaki kayıtlara erişilemez. Destek almak için kuruluş sahibinizle iletişime geçin." + }, "secretsAccessSuspended": { "message": "Askıya alınan kuruluşlara erişilemez. Lütfen yardım için kuruluşunuzun sahibiyle iletişime geçin." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Askıya alınan kuruluşlarda hizmet hesapları oluşturulamaz. Lütfen yardım için kuruluşunuzun sahibiyle iletişime geçin." }, - "disabledOrganizationFilterError": { - "message": "Askıya alınmış kuruluşlardaki kayıtlara erişilemez. Destek almak için kuruluş sahibinizle iletişime geçin." - }, "licenseIsExpired": { "message": "Lisans süresi doldu." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO tanımlayıcı" }, - "ssoIdentifierHintPartOne": { - "message": "SSO ile giriş yapmaları için bu kimliği üyelerinize sağlayın. Bu adımı atlamak için kurulum yapın ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "SSO bağlantısını kes" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Erişim engellendi. Bu sayfayı görüntüleme iznine sahip değilsiniz." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Bilinmeyen sır, bu sırra erişmek için izin istemeniz gerekebilir." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Bilinmeyen proje, bu projeye erişmek için izin istemeniz gerekebilir." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Grup/Kullanıcı" }, - "lowKdfIterations": { - "message": "Düşük KDF iterasyonu" - }, - "updateLowKdfIterationsDesc": { - "message": "Yeni güvenlik önerilerini karşılamak ve hesap korumasını iyileştirmek için şifreleme ayarlarınızı güncelleyin." - }, "kdfSettingsChangeLogoutWarning": { "message": "Devam ettiğinizde tüm aktif oturumlardan çıkış yapacaksınız. Tekrar oturum açmanız ve varsa iki aşamalı oturum açma işlemini tamamlamanız gerekecektir. Veri kaybını önlemek için şifreleme ayarlarınızı değiştirmeden önce kasayı dışa aktarmanızı öneririz." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Kimliği: $PROJECT_ID$ olan bir projeye erişildi.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Tanımlayıcısı: $PROJECT_ID$ olan bir proje düzenlendi", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Tanımlayıcısı: $PROJECT_ID$ olan bir proje silindi", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Ata" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Koleksiyonlara ata" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Entegrasyon kaydedilemedi. Lütfen daha sonra tekrar deneyin." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Entegrasyon silinemedi. Lütfen daha sonra tekrar deneyin." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Düşük KDF iterasyonları. Hesabınızın güvenliğini artırmak için iterasyonları artırın." - }, - "changeKDFSettings": { - "message": "KDF ayarlarını değiştir" - }, "secureYourInfrastructure": { "message": "Altyapınızı güvence altına alın" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Arşivde ara" }, - "archive": { - "message": "Arşiv" + "archiveNoun": { + "message": "Arşiv", + "description": "Noun" + }, + "archiveVerb": { + "message": "Arşivle", + "description": "Verb" }, "noItemsInArchive": { "message": "Arşivde kayıt yok" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden uzantısı yüklendi!" }, + "openTheBitwardenExtension": { + "message": "Bitwarden uzantısını aç" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Otomatik doldurmaya başlamak için uzantıyı açıp giriş yapın." }, diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index f68cf42627d..db32311e2ae 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "Створити новий запис" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "Критичні програми ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Сповіщення учасників ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Ризиковані учасники" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Неправильний головний пароль" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Неправильний пароль файлу. Використайте пароль, який ви вводили під час створення експортованого файлу." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Немає записів." }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "У вас немає повноважень на перегляд усіх записів у цій збірці." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Організацію призупинено" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "До призупинених організацій не можна отримати доступ. Зверніться до власника вашої організації для отримання допомоги." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "У призупинених організаціях не можна створювати службові облікові записи. Зверніться до власника вашої організації для отримання допомоги." }, - "disabledOrganizationFilterError": { - "message": "Записи у призупинених організаціях недоступні. Зверніться до власника вашої організації для отримання допомоги." - }, "licenseIsExpired": { "message": "Термін дії ліцензії завершився." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO-ідентифікатор" }, - "ssoIdentifierHintPartOne": { - "message": "Надайте цей ID своїм учасникам для входу з використанням SSO. Щоб обійти цей крок, налаштуйте ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Від'єднати SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Доступ заборонено. У вас немає дозволу на перегляд цієї сторінки." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Група/Користувач" }, - "lowKdfIterations": { - "message": "Низьке значення ітерацій KDF" - }, - "updateLowKdfIterationsDesc": { - "message": "Оновіть свої налаштування шифрування згідно з новими рекомендаціями щодо безпеки для вдосконалення захисту облікового запису." - }, "kdfSettingsChangeLogoutWarning": { "message": "Продовжуючи, ви вийдете з усіх активних сеансів. Необхідно буде повторно виконати вхід і пройти двоетапну перевірку, якщо вона увімкнена. Перед зміною налаштувань шифрування ми рекомендуємо експортувати ваше сховище, щоб запобігти можливій втраті даних." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Призначити" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Призначити до збірок" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Низьке значення ітерацій KDF. Збільште значення для посилення безпеки свого облікового запису." - }, - "changeKDFSettings": { - "message": "Змінити налаштування KDF" - }, "secureYourInfrastructure": { "message": "Захистіть свою інфраструктуру" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Розширення Bitwarden встановлено!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Відкрийте розширення, щоб увійти й користуватися автозаповненням." }, diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 9a87b3e2c6c..f5e3ee95bb6 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -59,8 +59,61 @@ "createNewLoginItem": { "message": "Tạo mục đăng nhập mới" }, - "criticalApplicationsActivityDescription": { - "message": "Khi bạn đánh dấu các ứng dụng là quan trọng, chúng sẽ hiển thị tại đây." + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { + "message": "Once you mark applications critical, they will display here." + }, + "viewAtRiskMembers": { + "message": "Xem các thành viên gặp rủi ro" + }, + "viewAtRiskApplications": { + "message": "Xem các ứng dụng gặp rủi ro" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ trên tổng số $TOTAL$ ứng dụng quan trọng đang gặp rủi ro do mật khẩu không an toàn", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } }, "criticalApplicationsWithCount": { "message": "Ứng dụng quan trọng ($COUNT$)", @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ ứng dụng gặp rủi ro", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "Các thành viên đã được thông báo ($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "Các thành viên có rủi ro" }, - "membersAtRiskActivityDescription": { - "message": "Thành viên có quyền chỉnh sửa đối với các mục có nguy cơ cho các ứng dụng quan trọng" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ thành viên có nguy cơ", + "membersAtRiskCount": { + "message": "$COUNT$ thành viên gặp rủi ro", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "Mật khẩu chính không hợp lệ" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Mật khẩu chính không hợp lệ. Xác nhận email của bạn là chính xác và tài khoản được tạo trên $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "Mật khẩu tập tin không hợp lệ, vui lòng sử dụng mật khẩu bạn đã nhập khi xuất tập tin." }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "Chưa có mục nào." }, + "noItemsInTrash": { + "message": "Không có mục nào trong thùng rác" + }, + "noItemsInTrashDesc": { + "message": "Các mục bạn xóa sẽ hiển thị tại đây và bị xóa vĩnh viễn sau 30 ngày" + }, + "noItemsInVault": { + "message": "Không có mục nào trong kho lưu trữ" + }, + "emptyVaultDescription": { + "message": "Kho lưu trữ không chỉ bảo vệ mật khẩu của bạn. Bạn có thể lưu trữ an toàn các thông tin đăng nhập, ID, thẻ và ghi chú tại đây." + }, + "emptyFavorites": { + "message": "Bạn chưa đánh dấu mục nào là mục yêu thích" + }, + "emptyFavoritesDesc": { + "message": "Thêm các mục thường dùng vào mục yêu thích để truy cập nhanh." + }, + "noSearchResults": { + "message": "Không có kết quả tìm kiếm nào được trả về" + }, + "clearFiltersOrTryAnother": { + "message": "Xóa bộ lọc hoặc thử cụm từ tìm kiếm khác" + }, "noPermissionToViewAllCollectionItems": { "message": "Bạn không có quyền xem tất cả mục trong bộ sưu tập này." }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "Tổ chức đã bị tạm ngưng" }, + "organizationIsSuspended": { + "message": "Tổ chức đã bị tạm dừng" + }, + "organizationIsSuspendedDesc": { + "message": "Không thể truy cập các mục trong tổ chức đã bị tạm ngưng. Vui lòng liên hệ với chủ sở hữu tổ chức của bạn để được hỗ trợ." + }, "secretsAccessSuspended": { "message": "Không thể truy cập các tổ chức đã bị tạm ngưng. Vui lòng liên hệ với chủ sở hữu tổ chức của bạn để được hỗ trợ." }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "Không thể tạo tài khoản dịch vụ trong các tổ chức đã bị tạm ngưng. Vui lòng liên hệ với chủ sở hữu tổ chức của bạn để được hỗ trợ." }, - "disabledOrganizationFilterError": { - "message": "Không thể truy cập các mục trong tổ chức đã bị tạm ngưng. Vui lòng liên hệ với chủ sở hữu tổ chức của bạn để được hỗ trợ." - }, "licenseIsExpired": { "message": "Giấy phép đã hết hạn." }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "Mã định danh SSO" }, - "ssoIdentifierHintPartOne": { - "message": "Cung cấp ID này cho các thành viên của bạn để đăng nhập bằng SSO. Để bỏ qua bước này, hãy thiết lập ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Cung cấp ID này cho thành viên để đăng nhập bằng SSO. Thành viên có thể bỏ qua nhập định danh này trong SSO nếu một tên miền đã xác nhận được thiết lập. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Tìm hiểu thêm", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "Hủy liên kết SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Chỉ kho lưu trữ tổ chức liên kết với $ORGANIZATION$ sẽ được xuất.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Chỉ kho lưu trữ tổ chức liên kết với $ORGANIZATION$ được xuất. Bộ sưu tập mục của tôi sẽ không được bao gồm.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "Truy cập bị từ chối. Bạn không có quyền xem trang này." }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Bí mật không xác định, bạn có thể cần yêu cầu quyền để truy cập bí mật này." }, + "unknownServiceAccount": { + "message": "Tài khoản máy không xác định, bạn có thể cần yêu cầu quyền truy cập vào tài khoản máy này." + }, "unknownProject": { "message": "Dự án không xác định, bạn có thể cần yêu cầu quyền để truy cập dự án này." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "Nhóm/Người dùng" }, - "lowKdfIterations": { - "message": "Số vòng lặp KDF thấp" - }, - "updateLowKdfIterationsDesc": { - "message": "Cập nhật cài đặt mã hóa của bạn để đáp ứng các khuyến nghị bảo mật mới và cải thiện bảo vệ tài khoản." - }, "kdfSettingsChangeLogoutWarning": { "message": "Tiếp tục sẽ đăng xuất bạn khỏi tất cả các phiên hoạt động. Bạn sẽ cần đăng nhập lại và hoàn tất đăng nhập hai bước (nếu có). Chúng tôi khuyến nghị xuất kho mật khẩu của bạn trước khi thay đổi cài đặt mã hóa để tránh mất dữ liệu." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Đã truy cập một dự án với ID: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Đã truy cập dự án với định danh: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Đã xóa Id tài khoản máy: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Đã chỉnh sửa một dự án với định danh: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Đã thêm người dùng: $USER_ID$ vào tài khoản máy với định danh: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Đã xóa người dùng: $USER_ID$ khỏi tài khoản máy với định danh: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Đã xóa nhóm: $GROUP_ID$ khỏi tài khoản máy với định danh: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Đã tạo tài khoản máy với định danh: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Đã thêm nhóm: $GROUP_ID$ vào tài khoản máy với định danh: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Đã tạo tài khoản máy với định danh: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Đã xóa một dự án với định danh: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "Gán" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Gán vào bộ sưu tập" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Không thể lưu tích hợp. Vui lòng thử lại sau." }, + "mustBeOrgOwnerToPerformAction": { + "message": "Bạn phải là chủ sở hữu tổ chức để thực hiện hành động này." + }, "failedToDeleteIntegration": { "message": "Không thể xóa tích hợp. Vui lòng thử lại sau." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "Số vòng lặp KDF thấp. Tăng số vòng lặp để cải thiện bảo mật cho tài khoản của bạn." - }, - "changeKDFSettings": { - "message": "Thay đổi cài đặt KDF" - }, "secureYourInfrastructure": { "message": "Bảo vệ hạ tầng của bạn" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Tìm kiếm kho lưu trữ" }, - "archive": { - "message": "Lưu trữ" + "archiveNoun": { + "message": "Lưu trữ", + "description": "Noun" + }, + "archiveVerb": { + "message": "Lưu trữ", + "description": "Verb" }, "noItemsInArchive": { "message": "Không có mục nào trong kho lưu trữ" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Đã cài đặt tiện ích mở rộng Bitwarden!" }, + "openTheBitwardenExtension": { + "message": "Mở tiện ích mở rộng Bitwarden" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "Tiện ích mở rộng Bitwarden đã được cài đặt! Mở tiện ích mở rộng để đăng nhập và bắt đầu tự động điền." + }, "openExtensionToAutofill": { "message": "Mở tiện ích mở rộng để đăng nhập và bắt đầu tự động điền." }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 38c585142df..12afbc9a33c 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "创建新的登录项目" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% 已完成", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "总计 $TOTAL$ 个中的 $COUNT$ 个安全任务已完成", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "密码修改进度" + }, + "assignMembersTasksToMonitorProgress": { + "message": "分配成员任务以监测进度" + }, + "onceYouReviewApplications": { + "message": "您审查应用程序并将其标记为关键后,它们将显示在这里。" + }, + "sendReminders": { + "message": "发送提醒" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "您将应用程序标记为关键后,它们将显示在这里。" }, + "viewAtRiskMembers": { + "message": "查看存在风险的成员" + }, + "viewAtRiskApplications": { + "message": "查看存在风险的应用程序" + }, + "criticalApplicationsAreAtRisk": { + "message": "总计 $TOTAL$ 个中的 $COUNT$ 个关键应用程序因密码风险而存在风险", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "关键应用程序 ($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ 个应用程序存在风险", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ 个密码存在风险", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "已通知的成员 ($COUNT$)", "placeholders": { @@ -134,10 +205,10 @@ "atRiskMembers": { "message": "存在风险的成员" }, - "membersAtRiskActivityDescription": { - "message": "对关键应用程序中存在风险的项目具有编辑权限的成员" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "对关键应用程序中存在风险的项目具有访问权限的成员" }, - "membersAtRisk": { + "membersAtRiskCount": { "message": "$COUNT$ 个成员存在风险", "placeholders": { "count": { @@ -959,7 +1030,7 @@ "message": "卡号" }, "copyFieldCipherName": { - "message": "复制 $CIPHERNAME$ 的 $FIELD$", + "message": "复制 $CIPHERNAME$ 中的 $FIELD$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "无效的主密码" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "无效的主密码。请确认您的电子邮箱正确无误,以及您的账户是在 $HOST$ 上创建的。", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "无效的文件密码,请使用您创建导出文件时输入的密码。" }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "没有可列出的项目。" }, + "noItemsInTrash": { + "message": "回收站中没有项目" + }, + "noItemsInTrashDesc": { + "message": "您删除的项目将显示在这里,并在 30 天后永久删除" + }, + "noItemsInVault": { + "message": "密码库中没有项目" + }, + "emptyVaultDescription": { + "message": "密码库不仅保护您的密码。在这里还可以安全地存储登录、ID、支付卡和笔记。" + }, + "emptyFavorites": { + "message": "您没有收藏任何项目" + }, + "emptyFavoritesDesc": { + "message": "将常用项目添加到收藏夹以便快速访问。" + }, + "noSearchResults": { + "message": "未返回搜索结果" + }, + "clearFiltersOrTryAnother": { + "message": "清除筛选或尝试其他搜索词" + }, "noPermissionToViewAllCollectionItems": { "message": "您没有查看此集合中的所有项目的权限。" }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "组织已暂停" }, + "organizationIsSuspended": { + "message": "组织已暂停" + }, + "organizationIsSuspendedDesc": { + "message": "无法访问已暂停组织中的项目。请联系您的组织所有者寻求帮助。" + }, "secretsAccessSuspended": { "message": "无法访问已暂停的组织。请联系您的组织所有者寻求帮助。" }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "无法在已暂停的组织中创建服务账户。请联系您的组织所有者寻求帮助。" }, - "disabledOrganizationFilterError": { - "message": "无法访问已暂停组织中的项目。请联系您的组织所有者寻求帮助。" - }, "licenseIsExpired": { "message": "授权已过期" }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO 标识符" }, - "ssoIdentifierHintPartOne": { - "message": "将此 ID 提供给您的成员以供 SSO 登录使用。要绕过此步骤,请设置 ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "请将此 ID 提供给您的成员以用于 SSO 登录。如果已设置声明域名,成员可以在 SSO 期间跳过输入此标识符。", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "进一步了解", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "取消链接 SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库。", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库,不包括我的项目集合。", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "访问被拒绝。您没有权限查看此页面。" }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "未知的机密,您可能需要请求权限才能访问此机密。" }, + "unknownServiceAccount": { + "message": "未知的机器账户,您可能需要请求权限才能访问此机器账户。" + }, "unknownProject": { "message": "未知的工程,您可能需要请求权限才能访问此工程。" }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "群组/用户" }, - "lowKdfIterations": { - "message": "较低的 KDF 迭代" - }, - "updateLowKdfIterationsDesc": { - "message": "更新您的加密设置以满足新的安全建议以及增强账户保护。" - }, "kdfSettingsChangeLogoutWarning": { "message": "接下来将会注销您所有的活动会话。您需要重新登录并完成两步登录(如果有)。我们建议您在更改加密设置前导出密码库,以防止数据丢失。" }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "访问了 ID 为 $PROJECT_ID$ 的工程。", + "accessedProjectWithIdentifier": { + "message": "访问了标识符为 $PROJECT_ID$ 的工程。", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "删除了 ID 为 $SERVICE_ACCOUNT_ID$ 的机器账户", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "编辑了标识符为 $PROJECT_ID$ 的工程", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "向标识符为 $SERVICE_ACCOUNT_ID$ 的机器账户添加了用户 $USER_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "从标识符为 $SERVICE_ACCOUNT_ID$ 的机器账户移除了用户 $USER_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "从标识符为 $SERVICE_ACCOUNT_ID$ 的机器账户移除了群组 $GROUP_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "创建了标识符为 $SERVICE_ACCOUNT_ID$ 的机器账户", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "向标识符为 $SERVICE_ACCOUNT_ID$ 的机器账户添加了群组 $GROUP_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "删除了标识符为 $SERVICE_ACCOUNT_ID$ 的机器账户", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "删除了标识符为 $PROJECT_ID$ 的工程", "placeholders": { @@ -8760,7 +8965,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "满分 $TOTAL$", + "message": "总计 $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -9357,6 +9562,9 @@ "assign": { "message": "分配" }, + "assignTasks": { + "message": "分配任务" + }, "assignToCollections": { "message": "分配到集合" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "保存集成失败。请稍后再试。" }, + "mustBeOrgOwnerToPerformAction": { + "message": "您必须是组织所有者才能执行此操作。" + }, "failedToDeleteIntegration": { "message": "删除集成失败。请稍后再试。" }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "KDF 迭代低。请增加迭代次数以提高您账户的安全性。" - }, - "changeKDFSettings": { - "message": "更改 KDF 设置" - }, "secureYourInfrastructure": { "message": "保护您的基础设施" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "搜索归档" }, - "archive": { - "message": "归档" + "archiveNoun": { + "message": "归档", + "description": "Noun" + }, + "archiveVerb": { + "message": "归档", + "description": "Verb" }, "noItemsInArchive": { "message": "归档中没有项目" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden 扩展已安装!" }, + "openTheBitwardenExtension": { + "message": "打开 Bitwarden 扩展" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "Bitwarden 扩展已安装!打开扩展以登录并开始自动填充。" + }, "openExtensionToAutofill": { "message": "打开扩展以登录并开始自动填充。" }, diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 4f9a78c2c02..15c05a9598c 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -59,9 +59,62 @@ "createNewLoginItem": { "message": "新增登入項目" }, - "criticalApplicationsActivityDescription": { + "percentageCompleted": { + "message": "$PERCENT$% complete", + "placeholders": { + "percent": { + "content": "$1", + "example": "75" + } + } + }, + "securityTasksCompleted": { + "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, + "passwordChangeProgress": { + "message": "Password change progress" + }, + "assignMembersTasksToMonitorProgress": { + "message": "Assign members tasks to monitor progress" + }, + "onceYouReviewApplications": { + "message": "Once you review applications and mark them as critical, they will display here." + }, + "sendReminders": { + "message": "Send reminders" + }, + "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Once you mark applications critical, they will display here." }, + "viewAtRiskMembers": { + "message": "View at-risk members" + }, + "viewAtRiskApplications": { + "message": "View at-risk applications" + }, + "criticalApplicationsAreAtRisk": { + "message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "5" + } + } + }, "criticalApplicationsWithCount": { "message": "重要應用程式($COUNT$)", "placeholders": { @@ -80,6 +133,24 @@ } } }, + "countOfApplicationsAtRisk": { + "message": "$COUNT$ applications at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "countOfAtRiskPasswords": { + "message": "$COUNT$ passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "notifiedMembersWithCount": { "message": "已被通知的成員($COUNT$)", "placeholders": { @@ -134,11 +205,11 @@ "atRiskMembers": { "message": "具有風險的成員" }, - "membersAtRiskActivityDescription": { - "message": "Members with edit access to at-risk items for critical applications" + "membersWithAccessToAtRiskItemsForCriticalApps": { + "message": "Members with access to at-risk items for critical applications" }, - "membersAtRisk": { - "message": "$COUNT$ members at risk", + "membersAtRiskCount": { + "message": "$COUNT$ members at-risk", "placeholders": { "count": { "content": "$1", @@ -1471,6 +1542,15 @@ "invalidMasterPassword": { "message": "無效的主密碼" }, + "invalidMasterPasswordConfirmEmailAndHost": { + "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "placeholders": { + "host": { + "content": "$1", + "example": "vault.bitwarden.com" + } + } + }, "invalidFilePassword": { "message": "檔案密碼無效,請使用您當初匯出檔案時輸入的密碼。" }, @@ -1480,6 +1560,30 @@ "noItemsInList": { "message": "沒有可列出的項目。" }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "noPermissionToViewAllCollectionItems": { "message": "您沒有檢視此集合中所有項目的權限。" }, @@ -4772,6 +4876,12 @@ "organizationIsDisabled": { "message": "組織已停用" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, "secretsAccessSuspended": { "message": "無法存取已停用的組織。請聯絡您組織的擁有者以獲取協助。" }, @@ -4784,9 +4894,6 @@ "serviceAccountsCannotCreate": { "message": "無法在已停用組織中新增服務帳戶。請聯絡您組織的擁有者以獲取協助。" }, - "disabledOrganizationFilterError": { - "message": "無法存取已停用組織中的項目。請聯絡您組織的擁有者以獲取協助。" - }, "licenseIsExpired": { "message": "授權已逾期。" }, @@ -5160,9 +5267,13 @@ "ssoIdentifier": { "message": "SSO 識別" }, - "ssoIdentifierHintPartOne": { - "message": "將此 ID 提供給您的成員以使用 SSO 登入。若要繞過此步驟,請設定 ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" + "ssoIdentifierHint": { + "message": "Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. ", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" + }, + "claimedDomainsLearnMore": { + "message": "Learn more", + "description": "This will be used as part of a larger sentence, broken up to include a link. The full sentence will read 'Provide this ID to your members to login with SSO. Members can skip entering this identifier during SSO if a claimed domain is set up. Learn more'" }, "unlinkSso": { "message": "取消連結 SSO" @@ -6975,6 +7086,24 @@ } } }, + "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, "accessDenied": { "message": "拒絕存取。您沒有檢視此頁面的權限。" }, @@ -7129,6 +7258,9 @@ "unknownSecret": { "message": "Unknown secret, you may need to request permission to access this secret." }, + "unknownServiceAccount": { + "message": "Unknown machine account, you may need to request permission to access this machine account." + }, "unknownProject": { "message": "Unknown project, you may need to request permission to access this project." }, @@ -8376,12 +8508,6 @@ "groupSlashUser": { "message": "群組/使用者" }, - "lowKdfIterations": { - "message": "較低的 KDF 迭代次數" - }, - "updateLowKdfIterationsDesc": { - "message": "更新您的加密設定以滿足新的安全建議以及提升帳號保護。" - }, "kdfSettingsChangeLogoutWarning": { "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, @@ -8485,8 +8611,8 @@ } } }, - "accessedProjectWithId": { - "message": "Accessed a project with Id: $PROJECT_ID$.", + "accessedProjectWithIdentifier": { + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8512,6 +8638,15 @@ } } }, + "nameUnavailableServiceAccountDeleted": { + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { @@ -8521,6 +8656,76 @@ } } }, + "addedUserToServiceAccountWithId": { + "message": "Added user: $USER_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedUserToServiceAccountWithId": { + "message": "Removed user: $USER_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "user_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "removedGroupFromServiceAccountWithId": { + "message": "Removed group: $GROUP_ID$ from machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountCreatedWithId": { + "message": "Created machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "addedGroupToServiceAccountId": { + "message": "Added group: $GROUP_ID$ to machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "group_id": { + "content": "$1", + "example": "4d34e8a8" + }, + "service_account_id": { + "content": "$2", + "example": "4d34e8a8" + } + } + }, + "serviceAccountDeletedWithId": { + "message": "Deleted machine account with identifier: $SERVICE_ACCOUNT_ID$", + "placeholders": { + "service_account_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { @@ -9357,6 +9562,9 @@ "assign": { "message": "指派" }, + "assignTasks": { + "message": "Assign tasks" + }, "assignToCollections": { "message": "Assign to collections" }, @@ -9755,6 +9963,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "mustBeOrgOwnerToPerformAction": { + "message": "You must be the organization owner to perform this action." + }, "failedToDeleteIntegration": { "message": "Failed to delete integration. Please try again later." }, @@ -9996,12 +10207,6 @@ } } }, - "lowKDFIterationsBanner": { - "message": "低 KDF 迭代。增加迭代次數以提高帳號的安全性。" - }, - "changeKDFSettings": { - "message": "變更 KDF 設定" - }, "secureYourInfrastructure": { "message": "保護你的基礎設施" }, @@ -11024,8 +11229,13 @@ "searchArchive": { "message": "Search archive" }, - "archive": { - "message": "Archive" + "archiveNoun": { + "message": "Archive", + "description": "Noun" + }, + "archiveVerb": { + "message": "Archive", + "description": "Verb" }, "noItemsInArchive": { "message": "No items in archive" @@ -11140,6 +11350,12 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, + "openTheBitwardenExtension": { + "message": "Open the Bitwarden extension" + }, + "bitwardenExtensionInstalledOpenExtension": { + "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." }, diff --git a/apps/web/webpack.base.js b/apps/web/webpack.base.js new file mode 100644 index 00000000000..2bfe0e27553 --- /dev/null +++ b/apps/web/webpack.base.js @@ -0,0 +1,431 @@ +const fs = require("fs"); +const path = require("path"); + +const { AngularWebpackPlugin } = require("@ngtools/webpack"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const HtmlWebpackInjector = require("html-webpack-injector"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const TerserPlugin = require("terser-webpack-plugin"); +const webpack = require("webpack"); + +const config = require("./config.js"); +const pjson = require("./package.json"); + +module.exports.getEnv = function getEnv() { + const ENV = process.env.ENV == null ? "development" : process.env.ENV; + const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV; + const LOGGING = process.env.LOGGING != "false"; + + return { ENV, NODE_ENV, LOGGING }; +}; + +/** + * + * @param {{ + * configName: string; + * app: { + * entry: string; + * entryModule: string; + * }; + * tsConfig: string; + * }} params + */ +module.exports.buildConfig = function buildConfig(params) { + const { ENV, NODE_ENV, LOGGING } = module.exports.getEnv(); + + const envConfig = config.load(ENV); + if (LOGGING) { + config.log(`Building web - ${params.configName} version`); + config.log(envConfig); + } + + const moduleRules = [ + { + test: /\.(html)$/, + loader: "html-loader", + }, + { + test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, + exclude: /loading(|-white).svg/, + generator: { + filename: "fonts/[name].[contenthash][ext]", + }, + type: "asset/resource", + }, + { + test: /\.(jpe?g|png|gif|svg|webp|avif)$/i, + exclude: /.*(bwi-font)\.svg/, + generator: { + filename: "images/[name][ext]", + }, + type: "asset/resource", + }, + { + test: /\.scss$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + }, + "css-loader", + "resolve-url-loader", + { + loader: "sass-loader", + options: { + sourceMap: true, + }, + }, + ], + }, + { + test: /\.css$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + }, + "css-loader", + "resolve-url-loader", + { + loader: "postcss-loader", + options: { + sourceMap: true, + }, + }, + ], + }, + { + test: /\.[cm]?js$/, + use: [ + { + loader: "babel-loader", + options: { + configFile: "../../babel.config.json", + cacheDirectory: NODE_ENV !== "production", + }, + }, + ], + }, + { + test: /\.[jt]sx?$/, + loader: "@ngtools/webpack", + }, + ]; + + const plugins = [ + new HtmlWebpackPlugin({ + template: "./src/index.html", + filename: "index.html", + chunks: ["theme_head", "app/polyfills", "app/vendor", "app/main", "styles"], + }), + new HtmlWebpackInjector(), + new HtmlWebpackPlugin({ + template: "./src/connectors/webauthn.html", + filename: "webauthn-connector.html", + chunks: ["connectors/webauthn", "styles"], + }), + new HtmlWebpackPlugin({ + template: "./src/connectors/webauthn-mobile.html", + filename: "webauthn-mobile-connector.html", + chunks: ["connectors/webauthn", "styles"], + }), + new HtmlWebpackPlugin({ + template: "./src/connectors/webauthn-fallback.html", + filename: "webauthn-fallback-connector.html", + chunks: ["connectors/webauthn-fallback", "styles"], + }), + new HtmlWebpackPlugin({ + template: "./src/connectors/sso.html", + filename: "sso-connector.html", + chunks: ["connectors/sso", "styles"], + }), + new HtmlWebpackPlugin({ + template: "./src/connectors/redirect.html", + filename: "redirect-connector.html", + chunks: ["connectors/redirect", "styles"], + }), + new HtmlWebpackPlugin({ + template: "./src/connectors/duo-redirect.html", + filename: "duo-redirect-connector.html", + chunks: ["connectors/duo-redirect", "styles"], + }), + new HtmlWebpackPlugin({ + template: "./src/404.html", + filename: "404.html", + chunks: ["styles"], + // 404 page is a wildcard, this ensures it uses absolute paths. + publicPath: "/", + }), + new CopyWebpackPlugin({ + patterns: [ + { from: "./src/.nojekyll" }, + { from: "./src/manifest.json" }, + { from: "./src/favicon.ico" }, + { from: "./src/browserconfig.xml" }, + { from: "./src/app-id.json" }, + { from: "./src/images", to: "images" }, + { from: "./src/videos", to: "videos" }, + { from: "./src/locales", to: "locales" }, + { from: "../../node_modules/qrious/dist/qrious.min.js", to: "scripts" }, + { from: "../../node_modules/braintree-web-drop-in/dist/browser/dropin.js", to: "scripts" }, + { + from: "./src/version.json", + transform(content, path) { + return content.toString().replace("process.env.APPLICATION_VERSION", pjson.version); + }, + }, + ], + }), + new MiniCssExtractPlugin({ + filename: "[name].[contenthash].css", + chunkFilename: "[id].[contenthash].css", + }), + new webpack.ProvidePlugin({ + process: "process/browser.js", + }), + new webpack.EnvironmentPlugin({ + ENV: ENV, + NODE_ENV: NODE_ENV === "production" ? "production" : "development", + APPLICATION_VERSION: pjson.version, + CACHE_TAG: Math.random().toString(36).substring(7), + URLS: envConfig["urls"] ?? {}, + STRIPE_KEY: envConfig["stripeKey"] ?? "", + BRAINTREE_KEY: envConfig["braintreeKey"] ?? "", + PAYPAL_CONFIG: envConfig["paypal"] ?? {}, + FLAGS: envConfig["flags"] ?? {}, + DEV_FLAGS: NODE_ENV === "development" ? envConfig["devFlags"] : {}, + ADDITIONAL_REGIONS: envConfig["additionalRegions"] ?? [], + }), + new AngularWebpackPlugin({ + tsconfig: params.tsConfig, + entryModule: params.app.entryModule, + sourceMap: true, + }), + ]; + + // ref: https://webpack.js.org/configuration/dev-server/#devserver + let certSuffix = fs.existsSync("dev-server.local.pem") ? ".local" : ".shared"; + const devServer = + NODE_ENV !== "development" + ? {} + : { + server: { + type: "https", + options: { + key: fs.readFileSync("dev-server" + certSuffix + ".pem"), + cert: fs.readFileSync("dev-server" + certSuffix + ".pem"), + }, + }, + // host: '192.168.1.9', + proxy: [ + { + context: ["/api"], + target: envConfig.dev?.proxyApi, + pathRewrite: { "^/api": "" }, + secure: false, + changeOrigin: true, + }, + { + context: ["/identity"], + target: envConfig.dev?.proxyIdentity, + pathRewrite: { "^/identity": "" }, + secure: false, + changeOrigin: true, + }, + { + context: ["/events"], + target: envConfig.dev?.proxyEvents, + pathRewrite: { "^/events": "" }, + secure: false, + changeOrigin: true, + }, + { + context: ["/notifications"], + target: envConfig.dev?.proxyNotifications, + pathRewrite: { "^/notifications": "" }, + secure: false, + changeOrigin: true, + ws: true, + }, + { + context: ["/icons"], + target: envConfig.dev?.proxyIcons, + pathRewrite: { "^/icons": "" }, + secure: false, + changeOrigin: true, + }, + ], + headers: (req) => { + if (!req.originalUrl.includes("connector.html")) { + return { + "Content-Security-Policy": ` + default-src 'self' + ;script-src + 'self' + 'wasm-unsafe-eval' + 'sha256-ryoU+5+IUZTuUyTElqkrQGBJXr1brEv6r2CA62WUw8w=' + https://js.stripe.com + https://js.braintreegateway.com + https://www.paypalobjects.com + ;style-src + 'self' + https://assets.braintreegateway.com + https://*.paypal.com + ${"'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='" /* date input polyfill */} + ${"'sha256-JVRXyYPueLWdwGwY9m/7u4QlZ1xeQdqUj2t8OVIzZE4='" /* date input polyfill */} + ${"'sha256-EnIJNDxVnh0++RytXJOkU0sqtLDFt1nYUDOfeJ5SKxg='" /* ng-select */} + ${"'sha256-dbBsIsz2pJ5loaLjhE6xWlmhYdjl6ghbwnGSCr4YObs='" /* cdk-virtual-scroll */} + ${"'sha256-S+uMh1G1SNQDAMG3seBmknQ26Wh+KSEoKdsNiy0joEE='" /* cdk-visually-hidden */} + ;img-src + 'self' + data: + https://icons.bitwarden.net + https://*.paypal.com + https://www.paypalobjects.com + https://q.stripe.com + https://haveibeenpwned.com + ;media-src + 'self' + https://assets.bitwarden.com + ;child-src + 'self' + https://js.stripe.com + https://assets.braintreegateway.com + https://*.paypal.com + https://*.duosecurity.com + ;frame-src + 'self' + https://js.stripe.com + https://assets.braintreegateway.com + https://*.paypal.com + https://*.duosecurity.com + ;connect-src + 'self' + ${envConfig.dev.wsConnectSrc ?? ""} + wss://notifications.bitwarden.com + https://notifications.bitwarden.com + https://cdn.bitwarden.net + https://api.pwnedpasswords.com + https://api.2fa.directory/v3/totp.json + https://api.stripe.com + https://www.paypal.com + https://api.sandbox.braintreegateway.com + https://api.braintreegateway.com + https://client-analytics.braintreegateway.com + https://*.braintree-api.com + https://*.blob.core.windows.net + http://127.0.0.1:10000 + https://app.simplelogin.io/api/alias/random/new + https://quack.duckduckgo.com/api/email/addresses + https://app.addy.io/api/v1/aliases + https://api.fastmail.com + https://api.forwardemail.net + http://localhost:5000 + ;object-src + 'self' + blob: + ;` + .replace(/\n/g, " ") + .replace(/ +(?= )/g, ""), + }; + } + }, + hot: false, + port: envConfig.dev?.port ?? 8080, + allowedHosts: envConfig.dev?.allowedHosts ?? "auto", + client: { + overlay: { + errors: true, + warnings: false, + runtimeErrors: false, + }, + }, + }; + + const webpackConfig = { + mode: NODE_ENV, + devtool: "source-map", + devServer: devServer, + target: "web", + entry: { + "app/polyfills": "./src/polyfills.ts", + "app/main": params.app.entry, + "connectors/webauthn": "./src/connectors/webauthn.ts", + "connectors/webauthn-fallback": "./src/connectors/webauthn-fallback.ts", + "connectors/sso": "./src/connectors/sso.ts", + "connectors/duo-redirect": "./src/connectors/duo-redirect.ts", + "connectors/redirect": "./src/connectors/redirect.ts", + styles: ["./src/scss/styles.scss", "./src/scss/tailwind.css"], + theme_head: "./src/theme.ts", + }, + cache: + NODE_ENV === "production" + ? false + : { + type: "filesystem", + allowCollectingMemory: true, + cacheDirectory: path.resolve(__dirname, "../../node_modules/.cache/webpack"), + buildDependencies: { + config: [__filename], + }, + }, + snapshot: { + unmanagedPaths: [path.resolve(__dirname, "../../node_modules/@bitwarden/")], + }, + optimization: { + splitChunks: { + cacheGroups: { + commons: { + test: /[\\/]node_modules[\\/]/, + name: "app/vendor", + chunks: (chunk) => { + return chunk.name === "app/main"; + }, + }, + }, + }, + minimize: NODE_ENV === "production", + minimizer: [ + new TerserPlugin({ + terserOptions: { + safari10: true, + // Replicate Angular CLI behaviour + compress: { + global_defs: { + ngDevMode: false, + ngI18nClosureMode: false, + }, + }, + }, + }), + ], + }, + resolve: { + extensions: [".ts", ".js"], + symlinks: false, + modules: [path.resolve("../../node_modules")], + fallback: { + buffer: false, + util: require.resolve("util/"), + assert: false, + url: false, + fs: false, + process: false, + path: require.resolve("path-browserify"), + }, + }, + output: { + filename: "[name].[contenthash].js", + path: path.resolve(__dirname, "build"), + clean: true, + }, + module: { + rules: moduleRules, + }, + experiments: { + asyncWebAssembly: true, + }, + plugins: plugins, + }; + + return webpackConfig; +}; diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index b311499dd55..e9d7bd46002 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -1,411 +1,10 @@ -const fs = require("fs"); -const path = require("path"); +const { buildConfig } = require("./webpack.base"); -const { AngularWebpackPlugin } = require("@ngtools/webpack"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const HtmlWebpackInjector = require("html-webpack-injector"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); -const TerserPlugin = require("terser-webpack-plugin"); -const webpack = require("webpack"); - -const config = require("./config.js"); -const pjson = require("./package.json"); - -const ENV = process.env.ENV == null ? "development" : process.env.ENV; -const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV; -const LOGGING = process.env.LOGGING != "false"; - -const envConfig = config.load(ENV); -if (LOGGING) { - config.log(envConfig); -} - -const moduleRules = [ - { - test: /\.(html)$/, - loader: "html-loader", - }, - { - test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, - exclude: /loading(|-white).svg/, - generator: { - filename: "fonts/[name].[contenthash][ext]", - }, - type: "asset/resource", - }, - { - test: /\.(jpe?g|png|gif|svg|webp|avif)$/i, - exclude: /.*(bwi-font)\.svg/, - generator: { - filename: "images/[name][ext]", - }, - type: "asset/resource", - }, - { - test: /\.scss$/, - use: [ - { - loader: MiniCssExtractPlugin.loader, - }, - "css-loader", - "resolve-url-loader", - { - loader: "sass-loader", - options: { - sourceMap: true, - }, - }, - ], - }, - { - test: /\.css$/, - use: [ - { - loader: MiniCssExtractPlugin.loader, - }, - "css-loader", - "resolve-url-loader", - { - loader: "postcss-loader", - options: { - sourceMap: true, - }, - }, - ], - }, - { - test: /\.[cm]?js$/, - use: [ - { - loader: "babel-loader", - options: { - configFile: "../../babel.config.json", - cacheDirectory: NODE_ENV !== "production", - }, - }, - ], - }, - { - test: /\.[jt]sx?$/, - loader: "@ngtools/webpack", - }, -]; - -const plugins = [ - new HtmlWebpackPlugin({ - template: "./src/index.html", - filename: "index.html", - chunks: ["theme_head", "app/polyfills", "app/vendor", "app/main", "styles"], - }), - new HtmlWebpackInjector(), - new HtmlWebpackPlugin({ - template: "./src/connectors/webauthn.html", - filename: "webauthn-connector.html", - chunks: ["connectors/webauthn", "styles"], - }), - new HtmlWebpackPlugin({ - template: "./src/connectors/webauthn-mobile.html", - filename: "webauthn-mobile-connector.html", - chunks: ["connectors/webauthn", "styles"], - }), - new HtmlWebpackPlugin({ - template: "./src/connectors/webauthn-fallback.html", - filename: "webauthn-fallback-connector.html", - chunks: ["connectors/webauthn-fallback", "styles"], - }), - new HtmlWebpackPlugin({ - template: "./src/connectors/sso.html", - filename: "sso-connector.html", - chunks: ["connectors/sso", "styles"], - }), - new HtmlWebpackPlugin({ - template: "./src/connectors/redirect.html", - filename: "redirect-connector.html", - chunks: ["connectors/redirect", "styles"], - }), - new HtmlWebpackPlugin({ - template: "./src/connectors/duo-redirect.html", - filename: "duo-redirect-connector.html", - chunks: ["connectors/duo-redirect", "styles"], - }), - new HtmlWebpackPlugin({ - template: "./src/404.html", - filename: "404.html", - chunks: ["styles"], - // 404 page is a wildcard, this ensures it uses absolute paths. - publicPath: "/", - }), - new CopyWebpackPlugin({ - patterns: [ - { from: "./src/.nojekyll" }, - { from: "./src/manifest.json" }, - { from: "./src/favicon.ico" }, - { from: "./src/browserconfig.xml" }, - { from: "./src/app-id.json" }, - { from: "./src/images", to: "images" }, - { from: "./src/videos", to: "videos" }, - { from: "./src/locales", to: "locales" }, - { from: "../../node_modules/qrious/dist/qrious.min.js", to: "scripts" }, - { from: "../../node_modules/braintree-web-drop-in/dist/browser/dropin.js", to: "scripts" }, - { - from: "./src/version.json", - transform(content, path) { - return content.toString().replace("process.env.APPLICATION_VERSION", pjson.version); - }, - }, - ], - }), - new MiniCssExtractPlugin({ - filename: "[name].[contenthash].css", - chunkFilename: "[id].[contenthash].css", - }), - new webpack.ProvidePlugin({ - process: "process/browser.js", - }), - new webpack.EnvironmentPlugin({ - ENV: ENV, - NODE_ENV: NODE_ENV === "production" ? "production" : "development", - APPLICATION_VERSION: pjson.version, - CACHE_TAG: Math.random().toString(36).substring(7), - URLS: envConfig["urls"] ?? {}, - STRIPE_KEY: envConfig["stripeKey"] ?? "", - BRAINTREE_KEY: envConfig["braintreeKey"] ?? "", - PAYPAL_CONFIG: envConfig["paypal"] ?? {}, - FLAGS: envConfig["flags"] ?? {}, - DEV_FLAGS: NODE_ENV === "development" ? envConfig["devFlags"] : {}, - ADDITIONAL_REGIONS: envConfig["additionalRegions"] ?? [], - }), - new AngularWebpackPlugin({ - tsconfig: "tsconfig.build.json", +module.exports = buildConfig({ + configName: "OSS", + app: { + entry: "./src/main.ts", entryModule: "src/app/app.module#AppModule", - sourceMap: true, - }), -]; - -// ref: https://webpack.js.org/configuration/dev-server/#devserver -let certSuffix = fs.existsSync("dev-server.local.pem") ? ".local" : ".shared"; -const devServer = - NODE_ENV !== "development" - ? {} - : { - server: { - type: "https", - options: { - key: fs.readFileSync("dev-server" + certSuffix + ".pem"), - cert: fs.readFileSync("dev-server" + certSuffix + ".pem"), - }, - }, - // host: '192.168.1.9', - proxy: [ - { - context: ["/api"], - target: envConfig.dev?.proxyApi, - pathRewrite: { "^/api": "" }, - secure: false, - changeOrigin: true, - }, - { - context: ["/identity"], - target: envConfig.dev?.proxyIdentity, - pathRewrite: { "^/identity": "" }, - secure: false, - changeOrigin: true, - }, - { - context: ["/events"], - target: envConfig.dev?.proxyEvents, - pathRewrite: { "^/events": "" }, - secure: false, - changeOrigin: true, - }, - { - context: ["/notifications"], - target: envConfig.dev?.proxyNotifications, - pathRewrite: { "^/notifications": "" }, - secure: false, - changeOrigin: true, - ws: true, - }, - { - context: ["/icons"], - target: envConfig.dev?.proxyIcons, - pathRewrite: { "^/icons": "" }, - secure: false, - changeOrigin: true, - }, - ], - headers: (req) => { - if (!req.originalUrl.includes("connector.html")) { - return { - "Content-Security-Policy": ` - default-src 'self' - ;script-src - 'self' - 'wasm-unsafe-eval' - 'sha256-ryoU+5+IUZTuUyTElqkrQGBJXr1brEv6r2CA62WUw8w=' - https://js.stripe.com - https://js.braintreegateway.com - https://www.paypalobjects.com - ;style-src - 'self' - https://assets.braintreegateway.com - https://*.paypal.com - ${"'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='" /* date input polyfill */} - ${"'sha256-JVRXyYPueLWdwGwY9m/7u4QlZ1xeQdqUj2t8OVIzZE4='" /* date input polyfill */} - ${"'sha256-EnIJNDxVnh0++RytXJOkU0sqtLDFt1nYUDOfeJ5SKxg='" /* ng-select */} - ${"'sha256-dbBsIsz2pJ5loaLjhE6xWlmhYdjl6ghbwnGSCr4YObs='" /* cdk-virtual-scroll */} - ${"'sha256-S+uMh1G1SNQDAMG3seBmknQ26Wh+KSEoKdsNiy0joEE='" /* cdk-visually-hidden */} - ;img-src - 'self' - data: - https://icons.bitwarden.net - https://*.paypal.com - https://www.paypalobjects.com - https://q.stripe.com - https://haveibeenpwned.com - ;media-src - 'self' - https://assets.bitwarden.com - ;child-src - 'self' - https://js.stripe.com - https://assets.braintreegateway.com - https://*.paypal.com - https://*.duosecurity.com - ;frame-src - 'self' - https://js.stripe.com - https://assets.braintreegateway.com - https://*.paypal.com - https://*.duosecurity.com - ;connect-src - 'self' - ${envConfig.dev.wsConnectSrc ?? ""} - wss://notifications.bitwarden.com - https://notifications.bitwarden.com - https://cdn.bitwarden.net - https://api.pwnedpasswords.com - https://api.2fa.directory/v3/totp.json - https://api.stripe.com - https://www.paypal.com - https://api.sandbox.braintreegateway.com - https://api.braintreegateway.com - https://client-analytics.braintreegateway.com - https://*.braintree-api.com - https://*.blob.core.windows.net - http://127.0.0.1:10000 - https://app.simplelogin.io/api/alias/random/new - https://quack.duckduckgo.com/api/email/addresses - https://app.addy.io/api/v1/aliases - https://api.fastmail.com - https://api.forwardemail.net - http://localhost:5000 - ;object-src - 'self' - blob: - ;` - .replace(/\n/g, " ") - .replace(/ +(?= )/g, ""), - }; - } - }, - hot: false, - port: envConfig.dev?.port ?? 8080, - allowedHosts: envConfig.dev?.allowedHosts ?? "auto", - client: { - overlay: { - errors: true, - warnings: false, - runtimeErrors: false, - }, - }, - }; - -const webpackConfig = { - mode: NODE_ENV, - devtool: "source-map", - devServer: devServer, - target: "web", - entry: { - "app/polyfills": "./src/polyfills.ts", - "app/main": "./src/main.ts", - "connectors/webauthn": "./src/connectors/webauthn.ts", - "connectors/webauthn-fallback": "./src/connectors/webauthn-fallback.ts", - "connectors/sso": "./src/connectors/sso.ts", - "connectors/duo-redirect": "./src/connectors/duo-redirect.ts", - "connectors/redirect": "./src/connectors/redirect.ts", - styles: ["./src/scss/styles.scss", "./src/scss/tailwind.css"], - theme_head: "./src/theme.ts", }, - cache: - NODE_ENV === "production" - ? false - : { - type: "filesystem", - allowCollectingMemory: true, - cacheDirectory: path.resolve(__dirname, "../../node_modules/.cache/webpack"), - buildDependencies: { - config: [__filename], - }, - }, - snapshot: { - unmanagedPaths: [path.resolve(__dirname, "../../node_modules/@bitwarden/")], - }, - optimization: { - splitChunks: { - cacheGroups: { - commons: { - test: /[\\/]node_modules[\\/]/, - name: "app/vendor", - chunks: (chunk) => { - return chunk.name === "app/main"; - }, - }, - }, - }, - minimize: NODE_ENV === "production", - minimizer: [ - new TerserPlugin({ - terserOptions: { - safari10: true, - // Replicate Angular CLI behaviour - compress: { - global_defs: { - ngDevMode: false, - ngI18nClosureMode: false, - }, - }, - }, - }), - ], - }, - resolve: { - extensions: [".ts", ".js"], - symlinks: false, - modules: [path.resolve("../../node_modules")], - fallback: { - buffer: false, - util: require.resolve("util/"), - assert: false, - url: false, - fs: false, - process: false, - path: require.resolve("path-browserify"), - }, - }, - output: { - filename: "[name].[contenthash].js", - path: path.resolve(__dirname, "build"), - clean: true, - }, - module: { - rules: moduleRules, - }, - experiments: { - asyncWebAssembly: true, - }, - plugins: plugins, -}; - -module.exports = webpackConfig; + tsConfig: "tsconfig.build.json", +}); diff --git a/bitwarden_license/bit-cli/src/bit-serve-configurator.ts b/bitwarden_license/bit-cli/src/bit-serve-configurator.ts index c669eb70920..71df651d9d0 100644 --- a/bitwarden_license/bit-cli/src/bit-serve-configurator.ts +++ b/bitwarden_license/bit-cli/src/bit-serve-configurator.ts @@ -16,9 +16,9 @@ export class BitServeConfigurator extends OssServeConfigurator { super(serviceContainer); } - override configureRouter(router: koaRouter): void { + override async configureRouter(router: koaRouter): Promise { // Register OSS endpoints - super.configureRouter(router); + await super.configureRouter(router); // Register bit endpoints this.serveDeviceApprovals(router); diff --git a/bitwarden_license/bit-cli/webpack.config.js b/bitwarden_license/bit-cli/webpack.config.js index 3e991f7971e..f746da40761 100644 --- a/bitwarden_license/bit-cli/webpack.config.js +++ b/bitwarden_license/bit-cli/webpack.config.js @@ -1,12 +1,48 @@ -const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); +const path = require("path"); +const { buildConfig } = require("../../apps/cli/webpack.base"); -// Re-use the OSS CLI webpack config -const webpackConfig = require("../../apps/cli/webpack.config"); +module.exports = (webpackConfig, context) => { + // Detect if called by Nx (context parameter exists) + const isNxBuild = context && context.options; -// Update paths to use the bit-cli entrypoint and tsconfig -webpackConfig.entry = { bw: "../../bitwarden_license/bit-cli/src/bw.ts" }; -webpackConfig.resolve.plugins = [ - new TsconfigPathsPlugin({ configFile: "../../bitwarden_license/bit-cli/tsconfig.json" }), -]; + if (isNxBuild) { + // Nx build configuration + const mode = context.options.mode || "development"; + if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = mode; + } + const ENV = (process.env.ENV = process.env.NODE_ENV); -module.exports = webpackConfig; + return buildConfig({ + configName: "Commercial", + entry: context.options.main || "bitwarden_license/bit-cli/src/bw.ts", + tsConfig: "tsconfig.base.json", + outputPath: path.resolve(context.context.root, context.options.outputPath), + mode: mode, + env: ENV, + modulesPath: [path.resolve("node_modules")], + localesPath: "apps/cli/src/locales", + externalsModulesDir: "node_modules", + watch: context.options.watch || false, + }); + } else { + // npm build configuration + if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = "development"; + } + const ENV = (process.env.ENV = process.env.NODE_ENV); + const mode = ENV; + + return buildConfig({ + configName: "Commercial", + entry: "../../bitwarden_license/bit-cli/src/bw.ts", + tsConfig: "../../bitwarden_license/bit-cli/tsconfig.json", + outputPath: path.resolve(__dirname, "../../apps/cli/build"), + mode: mode, + env: ENV, + modulesPath: [path.resolve("../../node_modules")], + localesPath: "../../apps/cli/src/locales", + externalsModulesDir: "../../node_modules", + }); + } +}; diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/configuration/datadog-configuration.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/configuration/datadog-configuration.ts new file mode 100644 index 00000000000..e788ebba7f2 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/configuration/datadog-configuration.ts @@ -0,0 +1,17 @@ +import { OrganizationIntegrationServiceType } from "../organization-integration-service-type"; + +export class DatadogConfiguration { + uri: string; + apiKey: string; + service: OrganizationIntegrationServiceType; + + constructor(uri: string, apiKey: string, service: string) { + this.uri = uri; + this.apiKey = apiKey; + this.service = service as OrganizationIntegrationServiceType; + } + + toString(): string { + return JSON.stringify(this); + } +} diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/datadog-template.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/datadog-template.ts new file mode 100644 index 00000000000..9aa6e34f478 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/datadog-template.ts @@ -0,0 +1,17 @@ +import { OrganizationIntegrationServiceType } from "../../organization-integration-service-type"; + +export class DatadogTemplate { + source_type_name = "Bitwarden"; + title: string = "#Title#"; + text: string = + "ActingUser: #ActingUserId#\nUser: #UserId#\nEvent: #Type#\nOrganization: #OrganizationId#\nPolicyId: #PolicyId#\nIpAddress: #IpAddress#\nDomainName: #DomainName#\nCipherId: #CipherId#\n"; + service: OrganizationIntegrationServiceType; + + constructor(service: string) { + this.service = service as OrganizationIntegrationServiceType; + } + + toString(): string { + return JSON.stringify(this); + } +} diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration.ts index abd1861caa9..2167694b720 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration.ts @@ -1,6 +1,7 @@ import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; import { OrganizationIntegration } from "./organization-integration"; +import { OrganizationIntegrationType } from "./organization-integration-type"; /** Integration or SDK */ export type Integration = { @@ -23,6 +24,7 @@ export type Integration = { canSetupConnection?: boolean; configuration?: string; template?: string; + integrationType?: OrganizationIntegrationType | null; // OrganizationIntegration organizationIntegration?: OrganizationIntegration | null; diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-configuration.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-configuration.ts index d4bbd30055f..0209460b630 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-configuration.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-configuration.ts @@ -4,6 +4,7 @@ import { OrganizationIntegrationId, } from "@bitwarden/common/types/guid"; +import { DatadogTemplate } from "./integration-configuration-config/configuration-template/datadog-template"; import { HecTemplate } from "./integration-configuration-config/configuration-template/hec-template"; import { WebhookTemplate } from "./integration-configuration-config/configuration-template/webhook-template"; import { WebhookIntegrationConfigurationConfig } from "./integration-configuration-config/webhook-integration-configuration-config"; @@ -14,7 +15,7 @@ export class OrganizationIntegrationConfiguration { eventType?: EventType | null; configuration?: WebhookIntegrationConfigurationConfig | null; filters?: string; - template?: HecTemplate | WebhookTemplate | null; + template?: HecTemplate | WebhookTemplate | DatadogTemplate | null; constructor( id: OrganizationIntegrationConfigurationId, @@ -22,7 +23,7 @@ export class OrganizationIntegrationConfiguration { eventType?: EventType | null, configuration?: WebhookIntegrationConfigurationConfig | null, filters?: string, - template?: HecTemplate | WebhookTemplate | null, + template?: HecTemplate | WebhookTemplate | DatadogTemplate | null, ) { this.id = id; this.integrationId = integrationId; diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-service-type.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-service-type.ts index dd1b4fb3f6c..e9e93adc0ff 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-service-type.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-service-type.ts @@ -1,5 +1,6 @@ export const OrganizationIntegrationServiceType = Object.freeze({ CrowdStrike: "CrowdStrike", + Datadog: "Datadog", } as const); export type OrganizationIntegrationServiceType = diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-type.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-type.ts index 1c98e174836..3cf68ee9b1d 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-type.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-type.ts @@ -4,6 +4,7 @@ export const OrganizationIntegrationType = Object.freeze({ Slack: 3, Webhook: 4, Hec: 5, + Datadog: 6, } as const); export type OrganizationIntegrationType = diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration.ts index abbe2271b30..d32c92a460a 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration.ts @@ -1,5 +1,6 @@ import { OrganizationIntegrationId } from "@bitwarden/common/types/guid"; +import { DatadogConfiguration } from "./configuration/datadog-configuration"; import { HecConfiguration } from "./configuration/hec-configuration"; import { WebhookConfiguration } from "./configuration/webhook-configuration"; import { OrganizationIntegrationConfiguration } from "./organization-integration-configuration"; @@ -10,14 +11,14 @@ export class OrganizationIntegration { id: OrganizationIntegrationId; type: OrganizationIntegrationType; serviceType: OrganizationIntegrationServiceType; - configuration: HecConfiguration | WebhookConfiguration | null; + configuration: HecConfiguration | WebhookConfiguration | DatadogConfiguration | null; integrationConfiguration: OrganizationIntegrationConfiguration[] = []; constructor( id: OrganizationIntegrationId, type: OrganizationIntegrationType, serviceType: OrganizationIntegrationServiceType, - configuration: HecConfiguration | WebhookConfiguration | null, + configuration: HecConfiguration | WebhookConfiguration | DatadogConfiguration | null, integrationConfiguration: OrganizationIntegrationConfiguration[] = [], ) { this.id = id; diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/datadog-organization-integration-service.spec.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/datadog-organization-integration-service.spec.ts new file mode 100644 index 00000000000..0545f95cb83 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/datadog-organization-integration-service.spec.ts @@ -0,0 +1,184 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; + +import { + OrganizationId, + OrganizationIntegrationConfigurationId, + OrganizationIntegrationId, +} from "@bitwarden/common/types/guid"; + +import { DatadogConfiguration } from "../models/configuration/datadog-configuration"; +import { DatadogTemplate } from "../models/integration-configuration-config/configuration-template/datadog-template"; +import { OrganizationIntegration } from "../models/organization-integration"; +import { OrganizationIntegrationConfiguration } from "../models/organization-integration-configuration"; +import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response"; +import { OrganizationIntegrationResponse } from "../models/organization-integration-response"; +import { OrganizationIntegrationServiceType } from "../models/organization-integration-service-type"; +import { OrganizationIntegrationType } from "../models/organization-integration-type"; + +import { DatadogOrganizationIntegrationService } from "./datadog-organization-integration-service"; +import { OrganizationIntegrationApiService } from "./organization-integration-api.service"; +import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service"; + +describe("DatadogOrganizationIntegrationService", () => { + let service: DatadogOrganizationIntegrationService; + const mockIntegrationApiService = mock(); + const mockIntegrationConfigurationApiService = + mock(); + const organizationId = "org-1" as OrganizationId; + const integrationId = "int-1" as OrganizationIntegrationId; + const configId = "conf-1" as OrganizationIntegrationConfigurationId; + const serviceType = OrganizationIntegrationServiceType.CrowdStrike; + const url = "https://example.com"; + const apiKey = "token"; + + beforeEach(() => { + service = new DatadogOrganizationIntegrationService( + mockIntegrationApiService, + mockIntegrationConfigurationApiService, + ); + + jest.resetAllMocks(); + }); + + it("should set organization integrations", (done) => { + mockIntegrationApiService.getOrganizationIntegrations.mockResolvedValue([]); + service.setOrganizationIntegrations(organizationId); + const subscription = service.integrations$.subscribe((integrations) => { + expect(integrations).toEqual([]); + subscription.unsubscribe(); + done(); + }); + }); + + it("should save a new Datadog integration", async () => { + service.setOrganizationIntegrations(organizationId); + + const integrationResponse = { + id: integrationId, + type: OrganizationIntegrationType.Datadog, + configuration: JSON.stringify({ url, apiKey, service: serviceType }), + } as OrganizationIntegrationResponse; + + const configResponse = { + id: configId, + template: JSON.stringify({ service: serviceType }), + } as OrganizationIntegrationConfigurationResponse; + + mockIntegrationApiService.createOrganizationIntegration.mockResolvedValue(integrationResponse); + mockIntegrationConfigurationApiService.createOrganizationIntegrationConfiguration.mockResolvedValue( + configResponse, + ); + + await service.saveDatadog(organizationId, serviceType, url, apiKey); + + const integrations = await firstValueFrom(service.integrations$); + expect(integrations.length).toBe(1); + expect(integrations[0].id).toBe(integrationId); + expect(integrations[0].serviceType).toBe(serviceType); + }); + + it("should throw error on organization ID mismatch in saveDatadog", async () => { + service.setOrganizationIntegrations("other-org" as OrganizationId); + await expect(service.saveDatadog(organizationId, serviceType, url, apiKey)).rejects.toThrow( + Error("Organization ID mismatch"), + ); + }); + + it("should update an existing Datadog integration", async () => { + service.setOrganizationIntegrations(organizationId); + + const integrationResponse = { + id: integrationId, + type: OrganizationIntegrationType.Datadog, + configuration: JSON.stringify({ url, apiKey, service: serviceType }), + } as OrganizationIntegrationResponse; + + const configResponse = { + id: configId, + template: JSON.stringify({ service: serviceType }), + } as OrganizationIntegrationConfigurationResponse; + + mockIntegrationApiService.updateOrganizationIntegration.mockResolvedValue(integrationResponse); + mockIntegrationConfigurationApiService.updateOrganizationIntegrationConfiguration.mockResolvedValue( + configResponse, + ); + + await service.updateDatadog(organizationId, integrationId, configId, serviceType, url, apiKey); + + const integrations = await firstValueFrom(service.integrations$); + expect(integrations.length).toBe(1); + expect(integrations[0].id).toBe(integrationId); + }); + + it("should throw error on organization ID mismatch in updateDatadog", async () => { + service.setOrganizationIntegrations("other-org" as OrganizationId); + await expect( + service.updateDatadog(organizationId, integrationId, configId, serviceType, url, apiKey), + ).rejects.toThrow(Error("Organization ID mismatch")); + }); + + it("should get integration by id", async () => { + service["_integrations$"].next([ + new OrganizationIntegration( + integrationId, + OrganizationIntegrationType.Datadog, + serviceType, + {} as DatadogConfiguration, + [], + ), + ]); + const integration = await service.getIntegrationById(integrationId); + expect(integration).not.toBeNull(); + expect(integration!.id).toBe(integrationId); + }); + + it("should get integration by service type", async () => { + service["_integrations$"].next([ + new OrganizationIntegration( + integrationId, + OrganizationIntegrationType.Datadog, + serviceType, + {} as DatadogConfiguration, + [], + ), + ]); + const integration = await service.getIntegrationByServiceType(serviceType); + expect(integration).not.toBeNull(); + expect(integration!.serviceType).toBe(serviceType); + }); + + it("should get integration configurations", async () => { + const config = new OrganizationIntegrationConfiguration( + configId, + integrationId, + null, + null, + "", + {} as DatadogTemplate, + ); + + service["_integrations$"].next([ + new OrganizationIntegration( + integrationId, + OrganizationIntegrationType.Datadog, + serviceType, + {} as DatadogConfiguration, + [config], + ), + ]); + const configs = await service.getIntegrationConfigurations(integrationId); + expect(configs).not.toBeNull(); + expect(configs![0].id).toBe(configId); + }); + + it("convertToJson should parse valid JSON", () => { + const obj = service.convertToJson<{ a: number }>('{"a":1}'); + expect(obj).toEqual({ a: 1 }); + }); + + it("convertToJson should return null for invalid JSON", () => { + const obj = service.convertToJson<{ a: number }>("invalid"); + expect(obj).toBeNull(); + }); +}); diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/datadog-organization-integration-service.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/datadog-organization-integration-service.ts new file mode 100644 index 00000000000..1fd5e9f8c06 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/datadog-organization-integration-service.ts @@ -0,0 +1,350 @@ +import { BehaviorSubject, firstValueFrom, map, Subject, switchMap, takeUntil, zip } from "rxjs"; + +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { + OrganizationId, + OrganizationIntegrationId, + OrganizationIntegrationConfigurationId, +} from "@bitwarden/common/types/guid"; + +import { DatadogConfiguration } from "../models/configuration/datadog-configuration"; +import { DatadogTemplate } from "../models/integration-configuration-config/configuration-template/datadog-template"; +import { OrganizationIntegration } from "../models/organization-integration"; +import { OrganizationIntegrationConfiguration } from "../models/organization-integration-configuration"; +import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request"; +import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response"; +import { OrganizationIntegrationRequest } from "../models/organization-integration-request"; +import { OrganizationIntegrationResponse } from "../models/organization-integration-response"; +import { OrganizationIntegrationServiceType } from "../models/organization-integration-service-type"; +import { OrganizationIntegrationType } from "../models/organization-integration-type"; + +import { OrganizationIntegrationApiService } from "./organization-integration-api.service"; +import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service"; + +export type DatadogModificationFailureReason = { + mustBeOwner: boolean; + success: boolean; +}; + +export class DatadogOrganizationIntegrationService { + private organizationId$ = new BehaviorSubject(null); + private _integrations$ = new BehaviorSubject([]); + private destroy$ = new Subject(); + + integrations$ = this._integrations$.asObservable(); + + private fetch$ = this.organizationId$ + .pipe( + switchMap(async (orgId) => { + if (orgId) { + const data$ = await this.setIntegrations(orgId); + return await firstValueFrom(data$); + } else { + return this._integrations$.getValue(); + } + }), + takeUntil(this.destroy$), + ) + .subscribe({ + next: (integrations) => { + this._integrations$.next(integrations); + }, + }); + + constructor( + private integrationApiService: OrganizationIntegrationApiService, + private integrationConfigurationApiService: OrganizationIntegrationConfigurationApiService, + ) {} + + /** + * Sets the organization Id and will trigger the retrieval of the + * integrations for a given org. + * @param orgId + */ + setOrganizationIntegrations(orgId: OrganizationId) { + this.organizationId$.next(orgId); + } + + /** + * Saves a new organization integration and updates the integrations$ observable + * @param organizationId id of the organization + * @param service service type of the integration + * @param url url of the service + * @param apiKey api token + */ + async saveDatadog( + organizationId: OrganizationId, + service: OrganizationIntegrationServiceType, + url: string, + apiKey: string, + ): Promise { + if (organizationId != this.organizationId$.getValue()) { + throw new Error("Organization ID mismatch"); + } + + try { + const datadogConfig = new DatadogConfiguration(url, apiKey, service); + const newIntegrationResponse = await this.integrationApiService.createOrganizationIntegration( + organizationId, + new OrganizationIntegrationRequest( + OrganizationIntegrationType.Datadog, + datadogConfig.toString(), + ), + ); + + const newTemplate = new DatadogTemplate(service); + const newIntegrationConfigResponse = + await this.integrationConfigurationApiService.createOrganizationIntegrationConfiguration( + organizationId, + newIntegrationResponse.id, + new OrganizationIntegrationConfigurationRequest(null, null, null, newTemplate.toString()), + ); + + const newIntegration = this.mapResponsesToOrganizationIntegration( + newIntegrationResponse, + newIntegrationConfigResponse, + ); + if (newIntegration !== null) { + this._integrations$.next([...this._integrations$.getValue(), newIntegration]); + } + return { mustBeOwner: false, success: true }; + } catch (error) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + return { mustBeOwner: true, success: false }; + } + throw error; + } + } + + /** + * Updates an existing organization integration and updates the integrations$ observable + * @param organizationId id of the organization + * @param OrganizationIntegrationId id of the organization integration + * @param OrganizationIntegrationConfigurationId id of the organization integration configuration + * @param service service type of the integration + * @param url url of the service + * @param apiKey api token + */ + async updateDatadog( + organizationId: OrganizationId, + OrganizationIntegrationId: OrganizationIntegrationId, + OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId, + service: OrganizationIntegrationServiceType, + url: string, + apiKey: string, + ): Promise { + if (organizationId != this.organizationId$.getValue()) { + throw new Error("Organization ID mismatch"); + } + + try { + const datadogConfig = new DatadogConfiguration(url, apiKey, service); + const updatedIntegrationResponse = + await this.integrationApiService.updateOrganizationIntegration( + organizationId, + OrganizationIntegrationId, + new OrganizationIntegrationRequest( + OrganizationIntegrationType.Datadog, + datadogConfig.toString(), + ), + ); + + const updatedTemplate = new DatadogTemplate(service); + const updatedIntegrationConfigResponse = + await this.integrationConfigurationApiService.updateOrganizationIntegrationConfiguration( + organizationId, + OrganizationIntegrationId, + OrganizationIntegrationConfigurationId, + new OrganizationIntegrationConfigurationRequest( + null, + null, + null, + updatedTemplate.toString(), + ), + ); + + const updatedIntegration = this.mapResponsesToOrganizationIntegration( + updatedIntegrationResponse, + updatedIntegrationConfigResponse, + ); + + if (updatedIntegration !== null) { + this._integrations$.next([...this._integrations$.getValue(), updatedIntegration]); + } + return { mustBeOwner: false, success: true }; + } catch (error) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + return { mustBeOwner: true, success: false }; + } + throw error; + } + } + + async deleteDatadog( + organizationId: OrganizationId, + OrganizationIntegrationId: OrganizationIntegrationId, + OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId, + ): Promise { + if (organizationId != this.organizationId$.getValue()) { + throw new Error("Organization ID mismatch"); + } + + try { + // delete the configuration first due to foreign key constraint + await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration( + organizationId, + OrganizationIntegrationId, + OrganizationIntegrationConfigurationId, + ); + + // delete the integration + await this.integrationApiService.deleteOrganizationIntegration( + organizationId, + OrganizationIntegrationId, + ); + + // update the local observable + const updatedIntegrations = this._integrations$ + .getValue() + .filter((i) => i.id !== OrganizationIntegrationId); + this._integrations$.next(updatedIntegrations); + + return { mustBeOwner: false, success: true }; + } catch (error) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + return { mustBeOwner: true, success: false }; + } + throw error; + } + } + + /** + * Gets a OrganizationIntegration for an OrganizationIntegrationId + * @param integrationId id of the integration + * @returns OrganizationIntegration or null + */ + // TODO: Move to base class when another service integration type is implemented + async getIntegrationById( + integrationId: OrganizationIntegrationId, + ): Promise { + return await firstValueFrom( + this.integrations$.pipe( + map((integrations) => integrations.find((i) => i.id === integrationId) || null), + ), + ); + } + + /** + * Gets a OrganizationIntegration for a service type + * @param serviceType type of the service + * @returns OrganizationIntegration or null + */ + // TODO: Move to base class when another service integration type is implemented + async getIntegrationByServiceType( + serviceType: OrganizationIntegrationServiceType, + ): Promise { + return await firstValueFrom( + this.integrations$.pipe( + map((integrations) => integrations.find((i) => i.serviceType === serviceType) || null), + ), + ); + } + + /** + * Gets a OrganizationIntegrationConfigurations for an integration ID + * @param integrationId id of the integration + * @returns OrganizationIntegration array or null + */ + // TODO: Move to base class when another service integration type is implemented + async getIntegrationConfigurations( + integrationId: OrganizationIntegrationId, + ): Promise { + return await firstValueFrom( + this.integrations$.pipe( + map((integrations) => { + const integration = integrations.find((i) => i.id === integrationId); + return integration ? integration.integrationConfiguration : null; + }), + ), + ); + } + + // TODO: Move to data models to be more explicit for future services + private mapResponsesToOrganizationIntegration( + integrationResponse: OrganizationIntegrationResponse, + configurationResponse: OrganizationIntegrationConfigurationResponse, + ): OrganizationIntegration | null { + const datadogConfig = this.convertToJson( + integrationResponse.configuration, + ); + const template = this.convertToJson(configurationResponse.template); + + if (!datadogConfig || !template) { + return null; + } + + const integrationConfig = new OrganizationIntegrationConfiguration( + configurationResponse.id, + integrationResponse.id, + null, + null, + "", + template, + ); + + return new OrganizationIntegration( + integrationResponse.id, + integrationResponse.type, + datadogConfig.service, + datadogConfig, + [integrationConfig], + ); + } + + // Could possibly be moved to a base service. All services would then assume that the + // integration configuration would always be an array and this datadog specific service + // would just assume a single entry. + private setIntegrations(orgId: OrganizationId) { + const results$ = zip(this.integrationApiService.getOrganizationIntegrations(orgId)).pipe( + switchMap(([responses]) => { + const integrations: OrganizationIntegration[] = []; + const promises: Promise[] = []; + + responses.forEach((integration) => { + if (integration.type === OrganizationIntegrationType.Datadog) { + const promise = this.integrationConfigurationApiService + .getOrganizationIntegrationConfigurations(orgId, integration.id) + .then((response) => { + // datadog events will only have one OrganizationIntegrationConfiguration + const config = response[0]; + + const orgIntegration = this.mapResponsesToOrganizationIntegration( + integration, + config, + ); + + if (orgIntegration !== null) { + integrations.push(orgIntegration); + } + }); + promises.push(promise); + } + }); + return Promise.all(promises).then(() => { + return integrations; + }); + }), + ); + + return results$; + } + + // TODO: Move to base service when necessary + convertToJson(jsonString?: string): T | null { + try { + return JSON.parse(jsonString || "") as T; + } catch { + return null; + } + } +} diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts index 6c6a086e0f5..ad9854c4b25 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts @@ -311,22 +311,24 @@ export class HecOrganizationIntegrationService { const promises: Promise[] = []; responses.forEach((integration) => { - const promise = this.integrationConfigurationApiService - .getOrganizationIntegrationConfigurations(orgId, integration.id) - .then((response) => { - // Hec events will only have one OrganizationIntegrationConfiguration - const config = response[0]; + if (integration.type === OrganizationIntegrationType.Hec) { + const promise = this.integrationConfigurationApiService + .getOrganizationIntegrationConfigurations(orgId, integration.id) + .then((response) => { + // Hec events will only have one OrganizationIntegrationConfiguration + const config = response[0]; - const orgIntegration = this.mapResponsesToOrganizationIntegration( - integration, - config, - ); + const orgIntegration = this.mapResponsesToOrganizationIntegration( + integration, + config, + ); - if (orgIntegration !== null) { - integrations.push(orgIntegration); - } - }); - promises.push(promise); + if (orgIntegration !== null) { + integrations.push(orgIntegration); + } + }); + promises.push(promise); + } }); return Promise.all(promises).then(() => { return integrations; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts index 3f679924df9..6afb0ee6815 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts @@ -9,7 +9,7 @@ import { import { ApplicationHealthReportDetail, OrganizationReportSummary, - RiskInsightsReportData, + RiskInsightsData, } from "../models/report-models"; import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response"; @@ -40,7 +40,7 @@ export function getTrimmedCipherUris(cipher: CipherView): string[] { const uniqueDomains = new Set(); - uris.forEach((u: { uri: string }) => { + uris.forEach((u: { uri: string | undefined }) => { const domain = Utils.getDomain(u.uri) ?? u.uri; uniqueDomains.add(domain); }); @@ -154,10 +154,12 @@ export function getApplicationReportDetail( * * @returns An empty report */ -export function createNewReportData(): RiskInsightsReportData { +export function createNewReportData(): RiskInsightsData { return { - data: [], - summary: createNewSummaryData(), + creationDate: new Date(), + reportData: [], + summaryData: createNewSummaryData(), + applicationData: [], }; } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts index 89293651a23..871db2b68ac 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts @@ -37,8 +37,10 @@ export interface PasswordHealthReportApplicationsRequest { export interface SaveRiskInsightsReportRequest { data: { organizationId: OrganizationId; - date: string; + creationDate: string; reportData: string; + summaryData: string; + applicationData: string; contentEncryptionKey: string; }; } @@ -58,9 +60,10 @@ export function isSaveRiskInsightsReportResponse(obj: any): obj is SaveRiskInsig export class GetRiskInsightsReportResponse extends BaseResponse { id: string; organizationId: OrganizationId; - // TODO Update to use creationDate from server - date: string; + creationDate: Date; reportData: EncString; + summaryData: EncString; + applicationData: EncString; contentEncryptionKey: EncString; constructor(response: any) { @@ -68,8 +71,10 @@ export class GetRiskInsightsReportResponse extends BaseResponse { this.id = this.getResponseProperty("organizationId"); this.organizationId = this.getResponseProperty("organizationId"); - this.date = this.getResponseProperty("date"); + this.creationDate = new Date(this.getResponseProperty("creationDate")); this.reportData = new EncString(this.getResponseProperty("reportData")); + this.summaryData = new EncString(this.getResponseProperty("summaryData")); + this.applicationData = new EncString(this.getResponseProperty("applicationData")); this.contentEncryptionKey = new EncString(this.getResponseProperty("contentEncryptionKey")); } } @@ -77,7 +82,7 @@ export class GetRiskInsightsReportResponse extends BaseResponse { export class GetRiskInsightsSummaryResponse extends BaseResponse { id: string; organizationId: OrganizationId; - encryptedData: EncString; // Decrypted as OrganizationReportSummary + encryptedSummary: EncString; // Decrypted as OrganizationReportSummary contentEncryptionKey: EncString; constructor(response: any) { @@ -85,7 +90,7 @@ export class GetRiskInsightsSummaryResponse extends BaseResponse { // TODO Handle taking array of summary data and converting to array this.id = this.getResponseProperty("id"); this.organizationId = this.getResponseProperty("organizationId"); - this.encryptedData = this.getResponseProperty("encryptedData"); + this.encryptedSummary = this.getResponseProperty("encryptedData"); this.contentEncryptionKey = this.getResponseProperty("contentEncryptionKey"); } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/index.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/index.ts index b8fcfe251ff..abe1f7200dc 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/index.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/index.ts @@ -1,3 +1,5 @@ export * from "./api-models.types"; export * from "./password-health"; +export * from "./report-data-service.types"; +export * from "./report-encryption.types"; export * from "./report-models"; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/mock-data.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/mock-data.ts new file mode 100644 index 00000000000..c790fc327a9 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/mock-data.ts @@ -0,0 +1,140 @@ +import { mock } from "jest-mock-extended"; + +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response"; + +import { ApplicationHealthReportDetailEnriched } from "./report-data-service.types"; +import { + ApplicationHealthReportDetail, + OrganizationReportApplication, + OrganizationReportSummary, +} from "./report-models"; + +const mockApplication1: ApplicationHealthReportDetail = { + applicationName: "application1.com", + passwordCount: 2, + atRiskPasswordCount: 1, + atRiskCipherIds: ["cipher-1"], + memberCount: 2, + atRiskMemberCount: 1, + memberDetails: [ + { + userGuid: "user-id-1", + userName: "tom", + email: "tom@application1.com", + cipherId: "cipher-1", + }, + ], + atRiskMemberDetails: [ + { + userGuid: "user-id-2", + userName: "tom", + email: "tom2@application1.com", + cipherId: "cipher-2", + }, + ], + cipherIds: ["cipher-1", "cipher-2"], +}; + +const mockApplication2: ApplicationHealthReportDetail = { + applicationName: "site2.application1.com", + passwordCount: 0, + atRiskPasswordCount: 0, + atRiskCipherIds: [], + memberCount: 0, + atRiskMemberCount: 0, + memberDetails: [], + atRiskMemberDetails: [], + cipherIds: [], +}; +const mockApplication3: ApplicationHealthReportDetail = { + applicationName: "application2.com", + passwordCount: 0, + atRiskPasswordCount: 0, + atRiskCipherIds: [], + memberCount: 0, + atRiskMemberCount: 0, + memberDetails: [], + atRiskMemberDetails: [], + cipherIds: [], +}; + +export const mockReportData: ApplicationHealthReportDetail[] = [ + mockApplication1, + mockApplication2, + mockApplication3, +]; + +export const mockSummaryData: OrganizationReportSummary = { + totalMemberCount: 5, + totalAtRiskMemberCount: 2, + totalApplicationCount: 3, + totalAtRiskApplicationCount: 1, + totalCriticalMemberCount: 1, + totalCriticalAtRiskMemberCount: 1, + totalCriticalApplicationCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: [], +}; +export const mockApplicationData: OrganizationReportApplication[] = [ + { + applicationName: "application1.com", + isCritical: true, + }, + { + applicationName: "application2.com", + isCritical: false, + }, +]; + +export const mockEnrichedReportData: ApplicationHealthReportDetailEnriched[] = [ + { ...mockApplication1, isMarkedAsCritical: true, ciphers: [] }, + { ...mockApplication2, isMarkedAsCritical: false, ciphers: [] }, +]; + +export const mockCipherViews: CipherView[] = [ + mock({ + id: "cipher-1", + type: CipherType.Login, + login: { password: "pass1", username: "user1", uris: [{ uri: "https://app.com/login" }] }, + isDeleted: false, + viewPassword: true, + }), + mock({ + id: "cipher-2", + type: CipherType.Login, + login: { password: "pass2", username: "user2", uris: [{ uri: "app.com/home" }] }, + isDeleted: false, + viewPassword: true, + }), + mock({ + id: "cipher-3", + type: CipherType.Login, + login: { password: "pass3", username: "user3", uris: [{ uri: "https://other.com" }] }, + isDeleted: false, + viewPassword: true, + }), +]; + +export const mockMemberDetails = [ + mock({ + cipherIds: ["cipher-1"], + userGuid: "user1", + userName: "User 1", + email: "user1@app.com", + }), + mock({ + cipherIds: ["cipher-2"], + userGuid: "user2", + userName: "User 2", + email: "user2@app.com", + }), + mock({ + cipherIds: ["cipher-3"], + userGuid: "user3", + userName: "User 3", + email: "user3@other.com", + }), +]; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts index e026a4475b7..8127ea41085 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts @@ -1,7 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeVariant } from "@bitwarden/components"; @@ -33,16 +31,6 @@ export type ExposedPasswordDetail = { exposedXTimes: number; } | null; -/* - * After data is encrypted, it is returned with the - * encryption key used to encrypt the data. - */ -export interface EncryptedDataWithKey { - organizationId: OrganizationId; - encryptedData: EncString; - contentEncryptionKey: EncString; -} - export type LEGACY_MemberDetailsFlat = { userGuid: string; userName: string; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-data-service.types.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-data-service.types.ts new file mode 100644 index 00000000000..6196c788ecd --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-data-service.types.ts @@ -0,0 +1,18 @@ +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { + ApplicationHealthReportDetail, + OrganizationReportApplication, + OrganizationReportSummary, +} from "./report-models"; + +export type ApplicationHealthReportDetailEnriched = ApplicationHealthReportDetail & { + isMarkedAsCritical: boolean; + ciphers: CipherView[]; +}; +export interface RiskInsightsEnrichedData { + reportData: ApplicationHealthReportDetailEnriched[]; + summaryData: OrganizationReportSummary; + applicationData: OrganizationReportApplication[]; + creationDate: Date; +} diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-encryption.types.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-encryption.types.ts new file mode 100644 index 00000000000..d5f2726d7ca --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-encryption.types.ts @@ -0,0 +1,32 @@ +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { OrganizationId } from "@bitwarden/common/types/guid"; + +import { + ApplicationHealthReportDetail, + OrganizationReportApplication, + OrganizationReportSummary, +} from "./report-models"; + +/* + * After data is encrypted, it is returned with the + * encryption key used to encrypt the data. + */ +export interface EncryptedDataWithKey { + organizationId: OrganizationId; + encryptedReportData: EncString; + encryptedSummaryData: EncString; + encryptedApplicationData: EncString; + contentEncryptionKey: EncString; +} + +export interface DecryptedReportData { + reportData: ApplicationHealthReportDetail[]; + summaryData: OrganizationReportSummary; + applicationData: OrganizationReportApplication[]; +} + +export interface EncryptedReportData { + encryptedReportData: EncString; + encryptedSummaryData: EncString; + encryptedApplicationData: EncString; +} diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts index 1758bb41b1b..564f483813a 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts @@ -131,11 +131,6 @@ export type ApplicationHealthReportDetail = { cipherIds: string[]; }; -export type ApplicationHealthReportDetailEnriched = ApplicationHealthReportDetail & { - isMarkedAsCritical: boolean; - ciphers: CipherView[]; -}; - /* * A list of applications and the count of * at risk passwords for each application @@ -148,12 +143,6 @@ export type AtRiskApplicationDetail = { // -------------------- Password Health Report Models -------------------- export type PasswordHealthReportApplicationId = Opaque; -// -------------------- Risk Insights Report Models -------------------- -export interface RiskInsightsReportData { - data: ApplicationHealthReportDetailEnriched[]; - summary: OrganizationReportSummary; -} - export type ReportScore = { label: string; badgeVariant: BadgeVariant; sortOrder: number }; export type ReportResult = CipherView & { @@ -162,8 +151,9 @@ export type ReportResult = CipherView & { scoreKey: number; }; -export type ReportDetailsAndSummary = { - data: ApplicationHealthReportDetailEnriched[]; - summary: OrganizationReportSummary; - dateCreated: Date; -}; +export interface RiskInsightsData { + creationDate: Date; + reportData: ApplicationHealthReportDetail[]; + summaryData: OrganizationReportSummary; + applicationData: OrganizationReportApplication[]; +} diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts index f1eebf81d73..42e4b8975c4 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts @@ -1,7 +1,10 @@ import { BehaviorSubject } from "rxjs"; +import { ApplicationHealthReportDetailEnriched } from "../models"; import { OrganizationReportSummary } from "../models/report-models"; +import { RiskInsightsDataService } from "./risk-insights-data.service"; + export class AllActivitiesService { /// This class is used to manage the summary of all applications /// and critical applications. @@ -19,9 +22,35 @@ export class AllActivitiesService { totalCriticalAtRiskApplicationCount: 0, newApplications: [], }); - reportSummary$ = this.reportSummarySubject$.asObservable(); + private allApplicationsDetailsSubject$: BehaviorSubject = + new BehaviorSubject([]); + allApplicationsDetails$ = this.allApplicationsDetailsSubject$.asObservable(); + + private atRiskPasswordsCountSubject$ = new BehaviorSubject(0); + atRiskPasswordsCount$ = this.atRiskPasswordsCountSubject$.asObservable(); + + private passwordChangeProgressMetricHasProgressBarSubject$ = new BehaviorSubject(false); + passwordChangeProgressMetricHasProgressBar$ = + this.passwordChangeProgressMetricHasProgressBarSubject$.asObservable(); + + constructor(private dataService: RiskInsightsDataService) { + // All application summary changes + this.dataService.reportResults$.subscribe((report) => { + if (report) { + this.setAllAppsReportSummary(report.summaryData); + this.setAllAppsReportDetails(report.reportData); + } + }); + // Critical application summary changes + this.dataService.criticalReportResults$.subscribe((report) => { + if (report) { + this.setCriticalAppsReportSummary(report.summaryData); + } + }); + } + setCriticalAppsReportSummary(summary: OrganizationReportSummary) { this.reportSummarySubject$.next({ ...this.reportSummarySubject$.getValue(), @@ -39,6 +68,21 @@ export class AllActivitiesService { totalAtRiskMemberCount: summary.totalAtRiskMemberCount, totalApplicationCount: summary.totalApplicationCount, totalAtRiskApplicationCount: summary.totalAtRiskApplicationCount, + newApplications: summary.newApplications, }); } + + setAllAppsReportDetails(applications: ApplicationHealthReportDetailEnriched[]) { + const totalAtRiskPasswords = applications.reduce( + (sum, app) => sum + app.atRiskPasswordCount, + 0, + ); + this.atRiskPasswordsCountSubject$.next(totalAtRiskPasswords); + + this.allApplicationsDetailsSubject$.next(applications); + } + + setPasswordChangeProgressMetricHasProgressBar(hasProgressBar: boolean) { + this.passwordChangeProgressMetricHasProgressBarSubject$.next(hasProgressBar); + } } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.spec.ts index 72d7e88fcab..28d670f226d 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.spec.ts @@ -82,9 +82,10 @@ describe("CriticalAppsService", () => { }); it("should exclude records that already exist", async () => { + const privateCriticalAppsSubject = service["criticalAppsListSubject$"]; // arrange // one record already exists - service.setAppsInListForOrg([ + privateCriticalAppsSubject.next([ { id: randomUUID() as PasswordHealthReportApplicationId, organizationId: SomeOrganization, @@ -145,6 +146,7 @@ describe("CriticalAppsService", () => { it("should get by org id", () => { const orgId = "some organization" as OrganizationId; + const privateCriticalAppsSubject = service["criticalAppsListSubject$"]; const response = [ { id: "id1", organizationId: "some organization", uri: "https://example.com" }, { id: "id2", organizationId: "some organization", uri: "https://example.org" }, @@ -155,13 +157,14 @@ describe("CriticalAppsService", () => { const orgKey$ = new BehaviorSubject(OrgRecords); keyService.orgKeys$.mockReturnValue(orgKey$); service.loadOrganizationContext(SomeOrganization, SomeUser); - service.setAppsInListForOrg(response); + privateCriticalAppsSubject.next(response); service.getAppsListForOrg(orgId as OrganizationId).subscribe((res) => { expect(res).toHaveLength(2); }); }); it("should drop a critical app", async () => { + const privateCriticalAppsSubject = service["criticalAppsListSubject$"]; // arrange const selectedUrl = "https://example.com"; @@ -175,7 +178,7 @@ describe("CriticalAppsService", () => { service.loadOrganizationContext(SomeOrganization, SomeUser); - service.setAppsInListForOrg(initialList); + privateCriticalAppsSubject.next(initialList); // act await service.dropCriticalApp(SomeOrganization, selectedUrl); @@ -193,6 +196,7 @@ describe("CriticalAppsService", () => { }); it("should not drop a critical app if it does not exist", async () => { + const privateCriticalAppsSubject = service["criticalAppsListSubject$"]; // arrange const selectedUrl = "https://nonexistent.com"; @@ -206,7 +210,7 @@ describe("CriticalAppsService", () => { service.loadOrganizationContext(SomeOrganization, SomeUser); - service.setAppsInListForOrg(initialList); + privateCriticalAppsSubject.next(initialList); // act await service.dropCriticalApp(SomeOrganization, selectedUrl); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts index 82001387bbd..b3b2f7c44e8 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts @@ -83,11 +83,6 @@ export class CriticalAppsService { .pipe(map((apps) => apps.filter((app) => app.organizationId === orgId))); } - // Reset the critical apps list - setAppsInListForOrg(apps: PasswordHealthReportApplicationsResponse[]) { - this.criticalAppsListSubject$.next(apps); - } - // Save the selected critical apps for a given organization async setCriticalApps(orgId: OrganizationId, selectedUrls: string[]) { if (orgId != this.organizationId.value) { diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts index 69d936d3016..53ee3ffa892 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts @@ -6,3 +6,4 @@ export * from "./risk-insights-api.service"; export * from "./risk-insights-report.service"; export * from "./risk-insights-data.service"; export * from "./all-activities.service"; +export * from "./security-tasks-api.service"; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/password-health.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/password-health.service.ts index 3904c4c3865..2ad9f1c7cfd 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/password-health.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/password-health.service.ts @@ -29,7 +29,7 @@ export class PasswordHealthService { filter((cipher) => this.isValidCipher(cipher)), mergeMap((cipher) => this.auditService - .passwordLeaked(cipher.login.password) + .passwordLeaked(cipher.login.password!) .then((exposedCount) => ({ cipher, exposedCount })), ), // [FIXME] ExposedDetails is can still return a null @@ -74,11 +74,11 @@ export class PasswordHealthService { // Check the username const userInput = this.isUserNameNotEmpty(cipher) - ? this.extractUsernameParts(cipher.login.username) + ? this.extractUsernameParts(cipher.login.username!) : undefined; const { score } = this.passwordStrengthService.getPasswordStrength( - cipher.login.password, + cipher.login.password!, undefined, // No email available in this context userInput, ); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts index 4eda92f0eb3..56246f3c3b6 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts @@ -7,6 +7,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" import { makeEncString } from "@bitwarden/common/spec"; import { OrganizationId, OrganizationReportId } from "@bitwarden/common/types/guid"; +import { EncryptedDataWithKey } from "../models"; import { GetRiskInsightsApplicationDataResponse, GetRiskInsightsReportResponse, @@ -14,7 +15,7 @@ import { SaveRiskInsightsReportRequest, SaveRiskInsightsReportResponse, } from "../models/api-models.types"; -import { EncryptedDataWithKey } from "../models/password-health"; +import { mockApplicationData, mockReportData, mockSummaryData } from "../models/mock-data"; import { RiskInsightsApiService } from "./risk-insights-api.service"; @@ -26,17 +27,21 @@ describe("RiskInsightsApiService", () => { const orgId = "org1" as OrganizationId; const mockReportId = "report-1"; const mockKey = "encryption-key-1"; - const mockData = "encrypted-data"; - const reportData = makeEncString("test").encryptedString?.toString() ?? ""; - const reportKey = makeEncString("test-key").encryptedString?.toString() ?? ""; + const mockReportKey = makeEncString("test-key"); - const saveRiskInsightsReportRequest: SaveRiskInsightsReportRequest = { + const mockReportEnc = makeEncString(JSON.stringify(mockReportData)); + const mockSummaryEnc = makeEncString(JSON.stringify(mockSummaryData)); + const mockApplicationsEnc = makeEncString(JSON.stringify(mockApplicationData)); + + const mockSaveRiskInsightsReportRequest: SaveRiskInsightsReportRequest = { data: { organizationId: orgId, - date: new Date().toISOString(), - reportData: reportData, - contentEncryptionKey: reportKey, + creationDate: new Date().toISOString(), + reportData: mockReportEnc.decryptedValue ?? "", + summaryData: mockReportEnc.decryptedValue ?? "", + applicationData: mockReportEnc.decryptedValue ?? "", + contentEncryptionKey: mockReportKey.decryptedValue ?? "", }, }; @@ -53,7 +58,9 @@ describe("RiskInsightsApiService", () => { id: mockId, organizationId: orgId, date: new Date().toISOString(), - reportData: mockData, + reportData: mockReportEnc, + summaryData: mockSummaryEnc, + applicationData: mockApplicationsEnc, contentEncryptionKey: mockKey, }; @@ -96,17 +103,17 @@ describe("RiskInsightsApiService", () => { }); it("saveRiskInsightsReport$ should call apiService.send with correct parameters", async () => { - mockApiService.send.mockReturnValue(Promise.resolve(saveRiskInsightsReportRequest)); + mockApiService.send.mockReturnValue(Promise.resolve(mockSaveRiskInsightsReportRequest)); const result = await firstValueFrom( - service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId), + service.saveRiskInsightsReport$(mockSaveRiskInsightsReportRequest, orgId), ); - expect(result).toEqual(new SaveRiskInsightsReportResponse(saveRiskInsightsReportRequest)); + expect(result).toEqual(new SaveRiskInsightsReportResponse(mockSaveRiskInsightsReportRequest)); expect(mockApiService.send).toHaveBeenCalledWith( "POST", `/reports/organizations/${orgId.toString()}`, - saveRiskInsightsReportRequest.data, + mockSaveRiskInsightsReportRequest.data, true, true, ); @@ -117,13 +124,13 @@ describe("RiskInsightsApiService", () => { mockApiService.send.mockReturnValue(Promise.reject(error)); await expect( - firstValueFrom(service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId)), + firstValueFrom(service.saveRiskInsightsReport$(mockSaveRiskInsightsReportRequest, orgId)), ).rejects.toEqual(error); expect(mockApiService.send).toHaveBeenCalledWith( "POST", `/reports/organizations/${orgId.toString()}`, - saveRiskInsightsReportRequest.data, + mockSaveRiskInsightsReportRequest.data, true, true, ); @@ -134,13 +141,13 @@ describe("RiskInsightsApiService", () => { mockApiService.send.mockReturnValue(Promise.reject(error)); await expect( - firstValueFrom(service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId)), + firstValueFrom(service.saveRiskInsightsReport$(mockSaveRiskInsightsReportRequest, orgId)), ).rejects.toEqual(error); expect(mockApiService.send).toHaveBeenCalledWith( "POST", `/reports/organizations/${orgId.toString()}`, - saveRiskInsightsReportRequest.data, + mockSaveRiskInsightsReportRequest.data, true, true, ); @@ -153,7 +160,7 @@ describe("RiskInsightsApiService", () => { { reportId: mockReportId, organizationId: orgId, - encryptedData: mockData, + encryptedData: mockReportData, contentEncryptionKey: mockKey, }, ]; @@ -175,8 +182,10 @@ describe("RiskInsightsApiService", () => { it("updateRiskInsightsSummary$ should call apiService.send with correct parameters and return an Observable", async () => { const data: EncryptedDataWithKey = { organizationId: orgId, - encryptedData: new EncString(mockData), contentEncryptionKey: new EncString(mockKey), + encryptedReportData: new EncString(JSON.stringify(mockReportData)), + encryptedSummaryData: new EncString(JSON.stringify(mockSummaryData)), + encryptedApplicationData: new EncString(JSON.stringify(mockApplicationData)), }; const reportId = "report123" as OrganizationReportId; @@ -199,7 +208,9 @@ describe("RiskInsightsApiService", () => { const reportId = "report123" as OrganizationReportId; const mockResponse: EncryptedDataWithKey | null = { organizationId: orgId, - encryptedData: new EncString(mockData), + encryptedReportData: new EncString(JSON.stringify(mockReportData)), + encryptedSummaryData: new EncString(JSON.stringify(mockSummaryData)), + encryptedApplicationData: new EncString(JSON.stringify(mockApplicationData)), contentEncryptionKey: new EncString(mockKey), }; @@ -217,21 +228,17 @@ describe("RiskInsightsApiService", () => { }); it("updateRiskInsightsApplicationData$ should call apiService.send with correct parameters and return an Observable", async () => { - const applicationData: EncryptedDataWithKey = { - organizationId: orgId, - encryptedData: new EncString(mockData), - contentEncryptionKey: new EncString(mockKey), - }; const reportId = "report123" as OrganizationReportId; + const mockApplication = mockApplicationData[0]; mockApiService.send.mockResolvedValueOnce(undefined); const result = await firstValueFrom( - service.updateRiskInsightsApplicationData$(applicationData, orgId, reportId), + service.updateRiskInsightsApplicationData$(mockApplication, orgId, reportId), ); expect(mockApiService.send).toHaveBeenCalledWith( "PATCH", `/reports/organizations/${orgId.toString()}/data/application/${reportId.toString()}`, - applicationData, + mockApplication, true, true, ); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts index 8f40ae91b47..99bf27506be 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts @@ -4,6 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { OrganizationId, OrganizationReportId } from "@bitwarden/common/types/guid"; +import { EncryptedDataWithKey, OrganizationReportApplication } from "../models"; import { GetRiskInsightsApplicationDataResponse, GetRiskInsightsReportResponse, @@ -11,7 +12,6 @@ import { SaveRiskInsightsReportRequest, SaveRiskInsightsReportResponse, } from "../models/api-models.types"; -import { EncryptedDataWithKey } from "../models/password-health"; export class RiskInsightsApiService { constructor(private apiService: ApiService) {} @@ -102,7 +102,7 @@ export class RiskInsightsApiService { } updateRiskInsightsApplicationData$( - applicationData: EncryptedDataWithKey, + applicationData: OrganizationReportApplication, orgId: OrganizationId, reportId: OrganizationReportId, ): Observable { diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts index 7038844998d..6b775f8432e 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts @@ -1,10 +1,12 @@ -import { BehaviorSubject, EMPTY, firstValueFrom, Observable, of } from "rxjs"; +import { BehaviorSubject, EMPTY, firstValueFrom, Observable, of, throwError } from "rxjs"; import { + catchError, distinctUntilChanged, exhaustMap, filter, finalize, map, + shareReplay, switchMap, tap, withLatestFrom, @@ -18,19 +20,13 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { - AppAtRiskMembersDialogParams, - AtRiskApplicationDetail, - AtRiskMemberDetail, - DrawerType, - DrawerDetails, - ApplicationHealthReportDetail, - ApplicationHealthReportDetailEnriched, - ReportDetailsAndSummary, -} from "../models/report-models"; +import { ApplicationHealthReportDetailEnriched } from "../models"; +import { RiskInsightsEnrichedData } from "../models/report-data-service.types"; +import { DrawerType, DrawerDetails, ApplicationHealthReportDetail } from "../models/report-models"; import { CriticalAppsService } from "./critical-apps.service"; import { RiskInsightsReportService } from "./risk-insights-report.service"; + export class RiskInsightsDataService { // -------------------------- Context state -------------------------- // Current user viewing risk insights @@ -45,16 +41,17 @@ export class RiskInsightsDataService { organizationDetails$ = this.organizationDetailsSubject.asObservable(); // -------------------------- Data ------------------------------------ - private applicationsSubject = new BehaviorSubject(null); - applications$ = this.applicationsSubject.asObservable(); + // TODO: Remove. Will use report results + private LEGACY_applicationsSubject = new BehaviorSubject( + null, + ); + LEGACY_applications$ = this.LEGACY_applicationsSubject.asObservable(); - private dataLastUpdatedSubject = new BehaviorSubject(null); - dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable(); - - criticalApps$ = this.criticalAppsService.criticalAppsList$; + // TODO: Remove. Will use date from report results + private LEGACY_dataLastUpdatedSubject = new BehaviorSubject(null); + dataLastUpdated$ = this.LEGACY_dataLastUpdatedSubject.asObservable(); // --------------------------- UI State ------------------------------------ - private isLoadingSubject = new BehaviorSubject(false); isLoading$ = this.isLoadingSubject.asObservable(); @@ -78,21 +75,52 @@ export class RiskInsightsDataService { // ------------------------- Report Variables ---------------- // The last run report details - private reportResultsSubject = new BehaviorSubject(null); + private reportResultsSubject = new BehaviorSubject(null); reportResults$ = this.reportResultsSubject.asObservable(); // Is a report being generated private isRunningReportSubject = new BehaviorSubject(false); isRunningReport$ = this.isRunningReportSubject.asObservable(); - // The error from report generation if there was an error + + // --------------------------- Critical Application data --------------------- + criticalReportResults$: Observable = of(null); constructor( private accountService: AccountService, private criticalAppsService: CriticalAppsService, private organizationService: OrganizationService, private reportService: RiskInsightsReportService, - ) {} + ) { + // Reload report if critical applications change + // This also handles the original report load + this.criticalAppsService.criticalAppsList$ + .pipe(withLatestFrom(this.organizationDetails$, this.userId$)) + .subscribe({ + next: ([_criticalApps, organizationDetails, userId]) => { + if (organizationDetails?.organizationId && userId) { + this.fetchLastReport(organizationDetails?.organizationId, userId); + } + }, + }); + + // Setup critical application data and summary generation for live critical application usage + this.criticalReportResults$ = this.reportResults$.pipe( + filter((report) => !!report), + map((r) => { + const criticalApplications = r.reportData.filter( + (application) => application.isMarkedAsCritical, + ); + const summary = this.reportService.generateApplicationsSummary(criticalApplications); + + return { + ...r, + summaryData: summary, + reportData: criticalApplications, + }; + }), + shareReplay({ bufferSize: 1, refCount: true }), + ); + } - // [FIXME] PM-25612 - Call Initialization in RiskInsightsComponent instead of child components async initializeForOrganization(organizationId: OrganizationId) { // Fetch current user const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); @@ -115,9 +143,6 @@ export class RiskInsightsDataService { // Load critical applications for organization await this.criticalAppsService.loadOrganizationContext(organizationId, userId); - // Load existing report - this.fetchLastReport(organizationId, userId); - // Setup new report generation this._runApplicationsReport().subscribe({ next: (result) => { @@ -133,7 +158,7 @@ export class RiskInsightsDataService { * Fetches the applications report and updates the applicationsSubject. * @param organizationId The ID of the organization. */ - fetchApplicationsReport(organizationId: OrganizationId, isRefresh?: boolean): void { + LEGACY_fetchApplicationsReport(organizationId: OrganizationId, isRefresh?: boolean): void { if (isRefresh) { this.isRefreshingSubject.next(true); } else { @@ -145,24 +170,20 @@ export class RiskInsightsDataService { finalize(() => { this.isLoadingSubject.next(false); this.isRefreshingSubject.next(false); - this.dataLastUpdatedSubject.next(new Date()); + this.LEGACY_dataLastUpdatedSubject.next(new Date()); }), ) .subscribe({ next: (reports: ApplicationHealthReportDetail[]) => { - this.applicationsSubject.next(reports); + this.LEGACY_applicationsSubject.next(reports); this.errorSubject.next(null); }, error: () => { - this.applicationsSubject.next([]); + this.LEGACY_applicationsSubject.next([]); }, }); } - refreshApplicationsReport(organizationId: OrganizationId): void { - this.fetchApplicationsReport(organizationId, true); - } - // ------------------------------- Enrichment methods ------------------------------- /** * Takes the basic application health report details and enriches them to include @@ -174,8 +195,10 @@ export class RiskInsightsDataService { enrichReportData$( applications: ApplicationHealthReportDetail[], ): Observable { + // TODO Compare applications on report to updated critical applications + // TODO Compare applications on report to any new applications return of(applications).pipe( - withLatestFrom(this.organizationDetails$, this.criticalApps$), + withLatestFrom(this.organizationDetails$, this.criticalAppsService.criticalAppsList$), switchMap(async ([apps, orgDetails, criticalApps]) => { if (!orgDetails) { return []; @@ -200,19 +223,11 @@ export class RiskInsightsDataService { ); } - // ------------------------------- Drawer management methods ------------------------------- // ------------------------- Drawer functions ----------------------------- - - isActiveDrawerType$ = (drawerType: DrawerType): Observable => { - return this.drawerDetails$.pipe(map((details) => details.activeDrawerType === drawerType)); - }; isActiveDrawerType = (drawerType: DrawerType): boolean => { return this.drawerDetailsSubject.value.activeDrawerType === drawerType; }; - isDrawerOpenForInvoker$ = (applicationName: string) => { - return this.drawerDetails$.pipe(map((details) => details.invokerId === applicationName)); - }; isDrawerOpenForInvoker = (applicationName: string): boolean => { return this.drawerDetailsSubject.value.invokerId === applicationName; }; @@ -228,10 +243,7 @@ export class RiskInsightsDataService { }); }; - setDrawerForOrgAtRiskMembers = ( - atRiskMemberDetails: AtRiskMemberDetail[], - invokerId: string = "", - ): void => { + setDrawerForOrgAtRiskMembers = async (invokerId: string = ""): Promise => { const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value; const shouldClose = open && activeDrawerType === DrawerType.OrgAtRiskMembers && currentInvokerId === invokerId; @@ -239,6 +251,15 @@ export class RiskInsightsDataService { if (shouldClose) { this.closeDrawer(); } else { + const reportResults = await firstValueFrom(this.reportResults$); + if (!reportResults) { + return; + } + + const atRiskMemberDetails = this.reportService.generateAtRiskMemberList( + reportResults.reportData, + ); + this.drawerDetailsSubject.next({ open: true, invokerId, @@ -250,10 +271,7 @@ export class RiskInsightsDataService { } }; - setDrawerForAppAtRiskMembers = ( - atRiskMembersDialogParams: AppAtRiskMembersDialogParams, - invokerId: string = "", - ): void => { + setDrawerForAppAtRiskMembers = async (invokerId: string = ""): Promise => { const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value; const shouldClose = open && activeDrawerType === DrawerType.AppAtRiskMembers && currentInvokerId === invokerId; @@ -261,21 +279,29 @@ export class RiskInsightsDataService { if (shouldClose) { this.closeDrawer(); } else { + const reportResults = await firstValueFrom(this.reportResults$); + if (!reportResults) { + return; + } + + const atRiskMembers = { + members: + reportResults.reportData.find((app) => app.applicationName === invokerId) + ?.atRiskMemberDetails ?? [], + applicationName: invokerId, + }; this.drawerDetailsSubject.next({ open: true, invokerId, activeDrawerType: DrawerType.AppAtRiskMembers, atRiskMemberDetails: [], - appAtRiskMembers: atRiskMembersDialogParams, + appAtRiskMembers: atRiskMembers, atRiskAppDetails: null, }); } }; - setDrawerForOrgAtRiskApps = ( - atRiskApps: AtRiskApplicationDetail[], - invokerId: string = "", - ): void => { + setDrawerForOrgAtRiskApps = async (invokerId: string = ""): Promise => { const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value; const shouldClose = open && activeDrawerType === DrawerType.OrgAtRiskApps && currentInvokerId === invokerId; @@ -283,13 +309,21 @@ export class RiskInsightsDataService { if (shouldClose) { this.closeDrawer(); } else { + const reportResults = await firstValueFrom(this.reportResults$); + if (!reportResults) { + return; + } + const atRiskAppDetails = this.reportService.generateAtRiskApplicationList( + reportResults.reportData, + ); + this.drawerDetailsSubject.next({ open: true, invokerId, activeDrawerType: DrawerType.OrgAtRiskApps, atRiskMemberDetails: [], appAtRiskMembers: null, - atRiskAppDetails: atRiskApps, + atRiskAppDetails, }); } }; @@ -311,23 +345,31 @@ export class RiskInsightsDataService { .getRiskInsightsReport$(organizationId, userId) .pipe( switchMap((report) => { - return this.enrichReportData$(report.data).pipe( + // Take fetched report data and merge with critical applications + return this.enrichReportData$(report.reportData).pipe( map((enrichedReport) => ({ - data: enrichedReport, - summary: report.summary, + report: enrichedReport, + summary: report.summaryData, + applications: report.applicationData, + creationDate: report.creationDate, })), ); }), + catchError((error: unknown) => { + // console.error("An error occurred when fetching the last report", error); + return EMPTY; + }), finalize(() => { this.isLoadingSubject.next(false); }), ) .subscribe({ - next: ({ data, summary }) => { + next: ({ report, summary, applications, creationDate }) => { this.reportResultsSubject.next({ - data, - summary, - dateCreated: new Date(), + reportData: report, + summaryData: summary, + applicationData: applications, + creationDate: creationDate, }); this.errorSubject.next(null); this.isLoadingSubject.next(false); @@ -343,6 +385,7 @@ export class RiskInsightsDataService { private _runApplicationsReport() { return this.isRunningReport$.pipe( distinctUntilChanged(), + // Only run this report if the flag for running is true filter((isRunning) => isRunning), withLatestFrom(this.organizationDetails$, this.userId$), exhaustMap(([_, organizationDetails, userId]) => { @@ -353,22 +396,30 @@ export class RiskInsightsDataService { // Generate the report return this.reportService.generateApplicationsReport$(organizationId).pipe( - map((data) => ({ - data, - summary: this.reportService.generateApplicationsSummary(data), + map((report) => ({ + report, + summary: this.reportService.generateApplicationsSummary(report), + applications: this.reportService.generateOrganizationApplications(report), })), - switchMap(({ data, summary }) => - this.enrichReportData$(data).pipe( - map((enrichedData) => ({ data: enrichedData, summary })), + // Enrich report with critical markings + switchMap(({ report, summary, applications }) => + this.enrichReportData$(report).pipe( + map((enrichedReport) => ({ report: enrichedReport, summary, applications })), ), ), - tap(({ data, summary }) => { - this.reportResultsSubject.next({ data, summary, dateCreated: new Date() }); + // Load the updated data into the UI + tap(({ report, summary, applications }) => { + this.reportResultsSubject.next({ + reportData: report, + summaryData: summary, + applicationData: applications, + creationDate: new Date(), + }); this.errorSubject.next(null); }), - switchMap(({ data, summary }) => { - // Just returns ID - return this.reportService.saveRiskInsightsReport$(data, summary, { + switchMap(({ report, summary, applications }) => { + // Save the generated data + return this.reportService.saveRiskInsightsReport$(report, summary, applications, { organizationId, userId, }); @@ -377,4 +428,42 @@ export class RiskInsightsDataService { }), ); } + + // ------------------------------ Critical application methods -------------- + + saveCriticalApplications(selectedUrls: string[]) { + return this.organizationDetails$.pipe( + exhaustMap((organizationDetails) => { + if (!organizationDetails?.organizationId) { + return EMPTY; + } + return this.criticalAppsService.setCriticalApps( + organizationDetails?.organizationId, + selectedUrls, + ); + }), + catchError((error: unknown) => { + this.errorSubject.next("Failed to save critical applications"); + return throwError(() => error); + }), + ); + } + + removeCriticalApplication(hostname: string) { + return this.organizationDetails$.pipe( + exhaustMap((organizationDetails) => { + if (!organizationDetails?.organizationId) { + return EMPTY; + } + return this.criticalAppsService.dropCriticalApp( + organizationDetails?.organizationId, + hostname, + ); + }), + catchError((error: unknown) => { + this.errorSubject.next("Failed to remove critical application"); + return throwError(() => error); + }), + ); + } } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.spec.ts index 9b7bb3b7258..e2c92ad4b9b 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.spec.ts @@ -10,6 +10,9 @@ import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { EncryptedReportData, DecryptedReportData } from "../models"; +import { mockApplicationData, mockReportData, mockSummaryData } from "../models/mock-data"; + import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service"; describe("RiskInsightsEncryptionService", () => { @@ -31,6 +34,10 @@ describe("RiskInsightsEncryptionService", () => { }; const orgKey$ = new BehaviorSubject(OrgRecords); + let mockDecryptedData: DecryptedReportData; + let mockEncryptedData: EncryptedReportData; + let mockKey: EncString; + beforeEach(() => { service = new RiskInsightsEncryptionService( mockKeyService, @@ -47,6 +54,18 @@ describe("RiskInsightsEncryptionService", () => { mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey); mockEncryptService.decryptString.mockResolvedValue(JSON.stringify(testData)); mockKeyService.orgKeys$.mockReturnValue(orgKey$); + + mockKey = new EncString("wrapped-key"); + mockEncryptedData = { + encryptedReportData: new EncString(JSON.stringify(mockReportData)), + encryptedSummaryData: new EncString(JSON.stringify(mockSummaryData)), + encryptedApplicationData: new EncString(JSON.stringify(mockApplicationData)), + }; + mockDecryptedData = { + reportData: mockReportData, + summaryData: mockSummaryData, + applicationData: mockApplicationData, + }; }); describe("encryptRiskInsightsReport", () => { @@ -55,22 +74,40 @@ describe("RiskInsightsEncryptionService", () => { mockKeyService.orgKeys$.mockReturnValue(orgKey$); // Act: call the method under test - const result = await service.encryptRiskInsightsReport(orgId, userId, testData); + const result = await service.encryptRiskInsightsReport( + { organizationId: orgId, userId }, + mockDecryptedData, + ); // Assert: ensure that the methods were called with the expected parameters expect(mockKeyService.orgKeys$).toHaveBeenCalledWith(userId); expect(mockKeyGenerationService.createKey).toHaveBeenCalledWith(512); + + // Assert all variables were encrypted expect(mockEncryptService.encryptString).toHaveBeenCalledWith( - JSON.stringify(testData), + JSON.stringify(mockDecryptedData.reportData), contentEncryptionKey, ); + expect(mockEncryptService.encryptString).toHaveBeenCalledWith( + JSON.stringify(mockDecryptedData.summaryData), + contentEncryptionKey, + ); + expect(mockEncryptService.encryptString).toHaveBeenCalledWith( + JSON.stringify(mockDecryptedData.applicationData), + contentEncryptionKey, + ); + expect(mockEncryptService.wrapSymmetricKey).toHaveBeenCalledWith( contentEncryptionKey, orgKey, ); + + // Mocked encrypt returns ENCRYPTED_TEXT expect(result).toEqual({ organizationId: orgId, - encryptedData: new EncString(ENCRYPTED_TEXT), + encryptedReportData: new EncString(ENCRYPTED_TEXT), + encryptedSummaryData: new EncString(ENCRYPTED_TEXT), + encryptedApplicationData: new EncString(ENCRYPTED_TEXT), contentEncryptionKey: new EncString(ENCRYPTED_KEY), }); }); @@ -82,9 +119,9 @@ describe("RiskInsightsEncryptionService", () => { mockEncryptService.wrapSymmetricKey.mockResolvedValue(new EncString(ENCRYPTED_KEY)); // Act & Assert: call the method under test and expect rejection - await expect(service.encryptRiskInsightsReport(orgId, userId, testData)).rejects.toThrow( - "Encryption failed, encrypted strings are null", - ); + await expect( + service.encryptRiskInsightsReport({ organizationId: orgId, userId }, mockDecryptedData), + ).rejects.toThrow("Encryption failed, encrypted strings are null"); }); it("should throw an error when encrypted key is null or empty", async () => { @@ -94,18 +131,18 @@ describe("RiskInsightsEncryptionService", () => { mockEncryptService.wrapSymmetricKey.mockResolvedValue(new EncString("")); // Act & Assert: call the method under test and expect rejection - await expect(service.encryptRiskInsightsReport(orgId, userId, testData)).rejects.toThrow( - "Encryption failed, encrypted strings are null", - ); + await expect( + service.encryptRiskInsightsReport({ organizationId: orgId, userId }, mockDecryptedData), + ).rejects.toThrow("Encryption failed, encrypted strings are null"); }); it("should throw if org key is not found", async () => { // when we cannot get an organization key, we should throw an error mockKeyService.orgKeys$.mockReturnValue(new BehaviorSubject({})); - await expect(service.encryptRiskInsightsReport(orgId, userId, testData)).rejects.toThrow( - "Organization key not found", - ); + await expect( + service.encryptRiskInsightsReport({ organizationId: orgId, userId }, mockDecryptedData), + ).rejects.toThrow("Organization key not found"); }); }); @@ -120,23 +157,21 @@ describe("RiskInsightsEncryptionService", () => { // actual decryption does not happen here, // we just want to ensure the method calls are correct const result = await service.decryptRiskInsightsReport( - orgId, - userId, - new EncString("encrypted-data"), - new EncString("wrapped-key"), - (data) => data as typeof testData, + { organizationId: orgId, userId }, + mockEncryptedData, + mockKey, ); expect(mockKeyService.orgKeys$).toHaveBeenCalledWith(userId); - expect(mockEncryptService.unwrapSymmetricKey).toHaveBeenCalledWith( - new EncString("wrapped-key"), - orgKey, - ); - expect(mockEncryptService.decryptString).toHaveBeenCalledWith( - new EncString("encrypted-data"), - contentEncryptionKey, - ); - expect(result).toEqual(testData); + expect(mockEncryptService.unwrapSymmetricKey).toHaveBeenCalledWith(mockKey, orgKey); + expect(mockEncryptService.decryptString).toHaveBeenCalledTimes(3); + + // Mock decrypt returns JSON.stringify(testData) + expect(result).toEqual({ + reportData: testData, + summaryData: testData, + applicationData: testData, + }); }); it("should invoke data type validation method during decryption", async () => { @@ -144,77 +179,47 @@ describe("RiskInsightsEncryptionService", () => { mockKeyService.orgKeys$.mockReturnValue(orgKey$); mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey); mockEncryptService.decryptString.mockResolvedValue(JSON.stringify(testData)); - const mockParseFn = jest.fn((data) => data as typeof testData); // act: call the decrypt method - with any params // actual decryption does not happen here, // we just want to ensure the method calls are correct const result = await service.decryptRiskInsightsReport( - orgId, - userId, - new EncString("encrypted-data"), - new EncString("wrapped-key"), - mockParseFn, + { organizationId: orgId, userId }, + mockEncryptedData, + mockKey, ); - expect(mockParseFn).toHaveBeenCalledWith(JSON.parse(JSON.stringify(testData))); - expect(result).toEqual(testData); + expect(result).toEqual({ + reportData: testData, + summaryData: testData, + applicationData: testData, + }); }); it("should return null if org key is not found", async () => { mockKeyService.orgKeys$.mockReturnValue(new BehaviorSubject({})); + await expect( + service.decryptRiskInsightsReport( + { organizationId: orgId, userId }, - const result = await service.decryptRiskInsightsReport( - orgId, - userId, - new EncString("encrypted-data"), - new EncString("wrapped-key"), - (data) => data as typeof testData, - ); - - expect(result).toBeNull(); + mockEncryptedData, + mockKey, + ), + ).rejects.toEqual(Error("Organization key not found")); }); it("should return null if decrypt throws", async () => { mockKeyService.orgKeys$.mockReturnValue(orgKey$); mockEncryptService.unwrapSymmetricKey.mockRejectedValue(new Error("fail")); - const result = await service.decryptRiskInsightsReport( - orgId, - userId, - new EncString("encrypted-data"), - new EncString("wrapped-key"), - (data) => data as typeof testData, - ); - expect(result).toBeNull(); - }); + await expect( + service.decryptRiskInsightsReport( + { organizationId: orgId, userId }, - it("should return null if decrypt throws", async () => { - mockKeyService.orgKeys$.mockReturnValue(orgKey$); - mockEncryptService.unwrapSymmetricKey.mockRejectedValue(new Error("fail")); - - const result = await service.decryptRiskInsightsReport( - orgId, - userId, - new EncString("encrypted-data"), - new EncString("wrapped-key"), - (data) => data as typeof testData, - ); - expect(result).toBeNull(); - }); - - it("should return null if decrypt throws", async () => { - mockKeyService.orgKeys$.mockReturnValue(orgKey$); - mockEncryptService.unwrapSymmetricKey.mockRejectedValue(new Error("fail")); - - const result = await service.decryptRiskInsightsReport( - orgId, - userId, - new EncString("encrypted-data"), - new EncString("wrapped-key"), - (data) => data as typeof testData, - ); - expect(result).toBeNull(); + mockEncryptedData, + mockKey, + ), + ).rejects.toEqual(Error("fail")); }); }); }); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.ts index 7bf01b04a63..04811f9cfcd 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-encryption.service.ts @@ -1,13 +1,13 @@ import { firstValueFrom, map } from "rxjs"; -import { Jsonify } from "type-fest"; import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; -import { EncryptedDataWithKey } from "../models/password-health"; +import { DecryptedReportData, EncryptedReportData, EncryptedDataWithKey } from "../models"; export class RiskInsightsEncryptionService { constructor( @@ -16,11 +16,15 @@ export class RiskInsightsEncryptionService { private keyGeneratorService: KeyGenerationService, ) {} - async encryptRiskInsightsReport( - organizationId: OrganizationId, - userId: UserId, - data: T, + async encryptRiskInsightsReport( + context: { + organizationId: OrganizationId; + userId: UserId; + }, + data: DecryptedReportData, + wrappedKey?: EncString, ): Promise { + const { userId, organizationId } = context; const orgKey = await firstValueFrom( this.keyService .orgKeys$(userId) @@ -35,10 +39,28 @@ export class RiskInsightsEncryptionService { throw new Error("Organization key not found"); } - const contentEncryptionKey = await this.keyGeneratorService.createKey(512); + let contentEncryptionKey: SymmetricCryptoKey; + if (!wrappedKey) { + // Generate a new key + contentEncryptionKey = await this.keyGeneratorService.createKey(512); + } else { + // Unwrap the existing key + contentEncryptionKey = await this.encryptService.unwrapSymmetricKey(wrappedKey, orgKey); + } - const dataEncrypted = await this.encryptService.encryptString( - JSON.stringify(data), + const { reportData, summaryData, applicationData } = data; + + // Encrypt the data + const encryptedReportData = await this.encryptService.encryptString( + JSON.stringify(reportData), + contentEncryptionKey, + ); + const encryptedSummaryData = await this.encryptService.encryptString( + JSON.stringify(summaryData), + contentEncryptionKey, + ); + const encryptedApplicationData = await this.encryptService.encryptString( + JSON.stringify(applicationData), contentEncryptionKey, ); @@ -47,59 +69,87 @@ export class RiskInsightsEncryptionService { orgKey, ); - if (!dataEncrypted.encryptedString || !wrappedEncryptionKey.encryptedString) { + if ( + !encryptedReportData.encryptedString || + !encryptedSummaryData.encryptedString || + !encryptedApplicationData.encryptedString || + !wrappedEncryptionKey.encryptedString + ) { throw new Error("Encryption failed, encrypted strings are null"); } - const encryptedData = dataEncrypted; - const contentEncryptionKeyString = wrappedEncryptionKey; - const encryptedDataPacket: EncryptedDataWithKey = { organizationId, - encryptedData, - contentEncryptionKey: contentEncryptionKeyString, + encryptedReportData: encryptedReportData, + encryptedSummaryData: encryptedSummaryData, + encryptedApplicationData: encryptedApplicationData, + contentEncryptionKey: wrappedEncryptionKey, }; return encryptedDataPacket; } - async decryptRiskInsightsReport( - organizationId: OrganizationId, - userId: UserId, - encryptedData: EncString, + async decryptRiskInsightsReport( + context: { + organizationId: OrganizationId; + userId: UserId; + }, + encryptedData: EncryptedReportData, wrappedKey: EncString, - parser: (data: Jsonify) => T, - ): Promise { - try { - const orgKey = await firstValueFrom( - this.keyService - .orgKeys$(userId) - .pipe( - map((organizationKeysById) => - organizationKeysById ? organizationKeysById[organizationId] : null, - ), + ): Promise { + const { userId, organizationId } = context; + const orgKey = await firstValueFrom( + this.keyService + .orgKeys$(userId) + .pipe( + map((organizationKeysById) => + organizationKeysById ? organizationKeysById[organizationId] : null, ), - ); + ), + ); - if (!orgKey) { - throw new Error("Organization key not found"); - } - - const unwrappedEncryptionKey = await this.encryptService.unwrapSymmetricKey( - wrappedKey, - orgKey, - ); - - const dataUnencrypted = await this.encryptService.decryptString( - encryptedData, - unwrappedEncryptionKey, - ); - - const dataUnencryptedJson = parser(JSON.parse(dataUnencrypted)); - - return dataUnencryptedJson as T; - } catch { - return null; + if (!orgKey) { + throw new Error("Organization key not found"); } + + const unwrappedEncryptionKey = await this.encryptService.unwrapSymmetricKey(wrappedKey, orgKey); + if (!unwrappedEncryptionKey) { + throw Error("Encryption key not found"); + } + + const { encryptedReportData, encryptedSummaryData, encryptedApplicationData } = encryptedData; + if (!encryptedReportData || !encryptedSummaryData || !encryptedApplicationData) { + throw new Error("Missing data"); + } + + // Decrypt the data + const decryptedReportData = await this.encryptService.decryptString( + encryptedReportData, + unwrappedEncryptionKey, + ); + const decryptedSummaryData = await this.encryptService.decryptString( + encryptedSummaryData, + unwrappedEncryptionKey, + ); + const decryptedApplicationData = await this.encryptService.decryptString( + encryptedApplicationData, + unwrappedEncryptionKey, + ); + + if (!decryptedReportData || !decryptedSummaryData || !decryptedApplicationData) { + throw new Error("Decryption failed, decrypted strings are null"); + } + + const decryptedReportDataJson = JSON.parse(decryptedReportData); + const decryptedSummaryDataJson = JSON.parse(decryptedSummaryData); + const decryptedApplicationDataJson = JSON.parse(decryptedApplicationData); + + const decryptedFullReport = { + reportData: decryptedReportDataJson, + summaryData: decryptedSummaryDataJson, + applicationData: decryptedApplicationDataJson, + }; + + return decryptedFullReport; } } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts index 18836fb1319..5f8fdaa244a 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts @@ -1,25 +1,23 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { makeEncString } from "@bitwarden/common/spec"; import { OrganizationId, 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 { createNewSummaryData } from "../helpers"; +import { DecryptedReportData, EncryptedDataWithKey } from "../models"; import { GetRiskInsightsReportResponse, SaveRiskInsightsReportResponse, } from "../models/api-models.types"; -import { EncryptedDataWithKey } from "../models/password-health"; import { - ApplicationHealthReportDetail, - OrganizationReportSummary, - RiskInsightsReportData, -} from "../models/report-models"; -import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response"; + mockApplicationData, + mockCipherViews, + mockMemberDetails, + mockReportData, + mockSummaryData, +} from "../models/mock-data"; import { mockCiphers } from "./ciphers.mock"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; @@ -45,17 +43,13 @@ describe("RiskInsightsReportService", () => { // Non changing mock data const mockOrganizationId = "orgId" as OrganizationId; const mockUserId = "userId" as UserId; - const ENCRYPTED_TEXT = "This data has been encrypted"; - const ENCRYPTED_KEY = "Re-encrypted Cipher Key"; - const mockEncryptedText = new EncString(ENCRYPTED_TEXT); - const mockEncryptedKey = new EncString(ENCRYPTED_KEY); + const mockEncryptedKey = makeEncString("test-key"); // Changing mock data - let mockCipherViews: CipherView[]; - let mockMemberDetails: MemberCipherDetailsResponse[]; - let mockReport: ApplicationHealthReportDetail[]; - let mockSummary: OrganizationReportSummary; - let mockEncryptedReport: EncryptedDataWithKey; + let mockDecryptedData: DecryptedReportData; + const mockReportEnc = makeEncString(JSON.stringify(mockReportData)); + const mockSummaryEnc = makeEncString(JSON.stringify(mockSummaryData)); + const mockApplicationsEnc = makeEncString(JSON.stringify(mockApplicationData)); beforeEach(() => { cipherService.getAllFromApiForOrganization.mockResolvedValue(mockCiphers); @@ -87,75 +81,15 @@ describe("RiskInsightsReportService", () => { service = new RiskInsightsReportService( cipherService, memberCipherDetailsService, + mockPasswordHealthService, mockRiskInsightsApiService, mockRiskInsightsEncryptionService, - mockPasswordHealthService, ); - // Reset mock ciphers before each test - mockCipherViews = [ - mock({ - id: "cipher-1", - type: CipherType.Login, - login: { password: "pass1", username: "user1", uris: [{ uri: "https://app.com/login" }] }, - isDeleted: false, - viewPassword: true, - }), - mock({ - id: "cipher-2", - type: CipherType.Login, - login: { password: "pass2", username: "user2", uris: [{ uri: "app.com/home" }] }, - isDeleted: false, - viewPassword: true, - }), - mock({ - id: "cipher-3", - type: CipherType.Login, - login: { password: "pass3", username: "user3", uris: [{ uri: "https://other.com" }] }, - isDeleted: false, - viewPassword: true, - }), - ]; - mockMemberDetails = [ - mock({ - cipherIds: ["cipher-1"], - userGuid: "user1", - userName: "User 1", - email: "user1@app.com", - }), - mock({ - cipherIds: ["cipher-2"], - userGuid: "user2", - userName: "User 2", - email: "user2@app.com", - }), - mock({ - cipherIds: ["cipher-3"], - userGuid: "user3", - userName: "User 3", - email: "user3@other.com", - }), - ]; - - mockReport = [ - { - applicationName: "app1", - passwordCount: 0, - atRiskPasswordCount: 0, - atRiskCipherIds: [], - memberCount: 0, - atRiskMemberCount: 0, - memberDetails: [], - atRiskMemberDetails: [], - cipherIds: [], - }, - ]; - mockSummary = createNewSummaryData(); - - mockEncryptedReport = { - organizationId: mockOrganizationId, - encryptedData: mockEncryptedText, - contentEncryptionKey: mockEncryptedKey, + mockDecryptedData = { + reportData: mockReportData, + summaryData: mockSummaryData, + applicationData: mockApplicationData, }; }); @@ -284,15 +218,22 @@ describe("RiskInsightsReportService", () => { describe("saveRiskInsightsReport$", () => { it("should not update subjects if save response does not have id", (done) => { + const mockEncryptedOutput: EncryptedDataWithKey = { + organizationId: mockOrganizationId, + encryptedReportData: mockReportEnc, + encryptedSummaryData: mockSummaryEnc, + encryptedApplicationData: mockApplicationsEnc, + contentEncryptionKey: mockEncryptedKey, + }; mockRiskInsightsEncryptionService.encryptRiskInsightsReport.mockResolvedValue( - mockEncryptedReport, + mockEncryptedOutput, ); const saveResponse = new SaveRiskInsightsReportResponse({ id: "" }); // Simulating no ID in response mockRiskInsightsApiService.saveRiskInsightsReport$.mockReturnValue(of(saveResponse)); service - .saveRiskInsightsReport$(mockReport, mockSummary, { + .saveRiskInsightsReport$(mockReportData, mockSummaryData, mockApplicationData, { organizationId: mockOrganizationId, userId: mockUserId, }) @@ -321,17 +262,19 @@ describe("RiskInsightsReportService", () => { it("should call with the correct organizationId", async () => { // we need to ensure that the api is invoked with the specified organizationId // here it doesn't matter what the Api returns - const apiResponse = { + const apiResponse = new GetRiskInsightsReportResponse({ id: "reportId", - date: new Date().toISOString(), + date: new Date(), organizationId: mockOrganizationId, - reportData: mockEncryptedReport.encryptedData, - contentEncryptionKey: mockEncryptedReport.contentEncryptionKey, - } as GetRiskInsightsReportResponse; + reportData: mockReportEnc.encryptedString, + summaryData: mockSummaryEnc.encryptedString, + applicationData: mockApplicationsEnc.encryptedString, + contentEncryptionKey: mockEncryptedKey.encryptedString, + }); - const decryptedResponse: RiskInsightsReportData = { - data: [], - summary: { + const decryptedResponse: DecryptedReportData = { + reportData: [], + summaryData: { totalMemberCount: 1, totalAtRiskMemberCount: 1, totalApplicationCount: 1, @@ -342,9 +285,9 @@ describe("RiskInsightsReportService", () => { totalCriticalAtRiskApplicationCount: 1, newApplications: [], }, + applicationData: [], }; - const organizationId = "orgId" as OrganizationId; const userId = "userId" as UserId; // Mock api returned encrypted data @@ -355,17 +298,15 @@ describe("RiskInsightsReportService", () => { Promise.resolve(decryptedResponse), ); - await firstValueFrom(service.getRiskInsightsReport$(organizationId, userId)); + await firstValueFrom(service.getRiskInsightsReport$(mockOrganizationId, userId)); expect(mockRiskInsightsApiService.getRiskInsightsReport$).toHaveBeenCalledWith( - organizationId, + mockOrganizationId, ); expect(mockRiskInsightsEncryptionService.decryptRiskInsightsReport).toHaveBeenCalledWith( - organizationId, - userId, - expect.anything(), // encryptedData - expect.anything(), // wrappedKey - expect.any(Function), // parser + { organizationId: mockOrganizationId, userId }, + expect.anything(), + expect.anything(), ); }); @@ -375,32 +316,29 @@ describe("RiskInsightsReportService", () => { const organizationId = "orgId" as OrganizationId; const userId = "userId" as UserId; - const mockResponse = { + const mockResponse = new GetRiskInsightsReportResponse({ id: "reportId", - date: new Date().toISOString(), + creationDate: new Date(), organizationId: organizationId as OrganizationId, - reportData: mockEncryptedReport.encryptedData, - contentEncryptionKey: mockEncryptedReport.contentEncryptionKey, - } as GetRiskInsightsReportResponse; + reportData: mockReportEnc.encryptedString, + summaryData: mockSummaryEnc.encryptedString, + applicationData: mockApplicationsEnc.encryptedString, + contentEncryptionKey: mockEncryptedKey.encryptedString, + }); - const decryptedReport = { - data: [{ foo: "bar" }], - }; mockRiskInsightsApiService.getRiskInsightsReport$.mockReturnValue(of(mockResponse)); mockRiskInsightsEncryptionService.decryptRiskInsightsReport.mockResolvedValue( - decryptedReport, + mockDecryptedData, ); const result = await firstValueFrom(service.getRiskInsightsReport$(organizationId, userId)); expect(mockRiskInsightsEncryptionService.decryptRiskInsightsReport).toHaveBeenCalledWith( - organizationId, - userId, + { organizationId: mockOrganizationId, userId }, expect.anything(), expect.anything(), - expect.any(Function), ); - expect(result).toEqual(decryptedReport); + expect(result).toEqual({ ...mockDecryptedData, creationDate: mockResponse.creationDate }); }); }); }); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts index d82366c0154..fcfc7a255df 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts @@ -1,6 +1,7 @@ import { - BehaviorSubject, + catchError, concatMap, + EMPTY, first, firstValueFrom, forkJoin, @@ -19,7 +20,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { createNewReportData, - createNewSummaryData, flattenMemberDetails, getApplicationReportDetail, getFlattenedCipherDetails, @@ -45,7 +45,8 @@ import { CipherHealthReport, MemberDetails, PasswordHealthData, - RiskInsightsReportData, + OrganizationReportApplication, + RiskInsightsData, } from "../models/report-models"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; @@ -54,14 +55,6 @@ import { RiskInsightsApiService } from "./risk-insights-api.service"; import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service"; export class RiskInsightsReportService { - private riskInsightsReportSubject = new BehaviorSubject([]); - riskInsightsReport$ = this.riskInsightsReportSubject.asObservable(); - - private riskInsightsSummarySubject = new BehaviorSubject( - createNewSummaryData(), - ); - riskInsightsSummary$ = this.riskInsightsSummarySubject.asObservable(); - // [FIXME] CipherData // Cipher data // private _ciphersSubject = new BehaviorSubject(null); @@ -70,9 +63,9 @@ export class RiskInsightsReportService { constructor( private cipherService: CipherService, private memberCipherDetailsApiService: MemberCipherDetailsApiService, + private passwordHealthService: PasswordHealthService, private riskInsightsApiService: RiskInsightsApiService, private riskInsightsEncryptionService: RiskInsightsEncryptionService, - private passwordHealthService: PasswordHealthService, ) {} // [FIXME] CipherData @@ -152,6 +145,7 @@ export class RiskInsightsReportService { /** * Report data for the aggregation of uris to like uris and getting password/member counts, * members, and at risk statuses. + * * @param organizationId Id of the organization * @returns The all applications health report data */ @@ -232,20 +226,51 @@ export class RiskInsightsReportService { const atRiskMembers = reports.flatMap((x) => x.atRiskMemberDetails); const uniqueAtRiskMembers = getUniqueMembers(atRiskMembers); - // TODO: totalCriticalMemberCount, totalCriticalAtRiskMemberCount, totalCriticalApplicationCount, totalCriticalAtRiskApplicationCount, and newApplications will be handled with future logic implementation + // TODO: Replace with actual new applications detection logic (PM-26185) + const dummyNewApplications = [ + "github.com", + "google.com", + "stackoverflow.com", + "gitlab.com", + "bitbucket.org", + "npmjs.com", + "docker.com", + "aws.amazon.com", + "azure.microsoft.com", + "jenkins.io", + "terraform.io", + "kubernetes.io", + "atlassian.net", + ]; + return { totalMemberCount: uniqueMembers.length, - totalCriticalMemberCount: 0, totalAtRiskMemberCount: uniqueAtRiskMembers.length, - totalCriticalAtRiskMemberCount: 0, totalApplicationCount: reports.length, - totalCriticalApplicationCount: 0, totalAtRiskApplicationCount: reports.filter((app) => app.atRiskPasswordCount > 0).length, + totalCriticalMemberCount: 0, + totalCriticalAtRiskMemberCount: 0, + totalCriticalApplicationCount: 0, totalCriticalAtRiskApplicationCount: 0, - newApplications: [], + newApplications: dummyNewApplications, }; } + /** + * Generate a snapshot of applications and related data associated to this report + * + * @param reports + * @returns A list of applications with a critical marking flag + */ + generateOrganizationApplications( + reports: ApplicationHealthReportDetail[], + ): OrganizationReportApplication[] { + return reports.map((report) => ({ + applicationName: report.applicationName, + isCritical: false, + })); + } + async identifyCiphers( data: ApplicationHealthReportDetail[], organizationId: OrganizationId, @@ -272,12 +297,12 @@ export class RiskInsightsReportService { getRiskInsightsReport$( organizationId: OrganizationId, userId: UserId, - ): Observable { + ): Observable { return this.riskInsightsApiService.getRiskInsightsReport$(organizationId).pipe( - switchMap((response): Observable => { + switchMap((response) => { if (!response) { // Return an empty report and summary if response is falsy - return of(createNewReportData()); + return of(createNewReportData()); } if (!response.contentEncryptionKey || response.contentEncryptionKey.data == "") { return throwError(() => new Error("Report key not found")); @@ -285,15 +310,43 @@ export class RiskInsightsReportService { if (!response.reportData) { return throwError(() => new Error("Report data not found")); } + if (!response.summaryData) { + return throwError(() => new Error("Summary data not found")); + } + if (!response.applicationData) { + return throwError(() => new Error("Application data not found")); + } + return from( - this.riskInsightsEncryptionService.decryptRiskInsightsReport( - organizationId, - userId, - response.reportData, + this.riskInsightsEncryptionService.decryptRiskInsightsReport( + { + organizationId, + userId, + }, + { + encryptedReportData: response.reportData, + encryptedSummaryData: response.summaryData, + encryptedApplicationData: response.applicationData, + }, response.contentEncryptionKey, - (data) => data as RiskInsightsReportData, ), - ).pipe(map((decryptedReport) => decryptedReport ?? createNewReportData())); + ).pipe( + map((decryptedData) => ({ + reportData: decryptedData.reportData, + summaryData: decryptedData.summaryData, + applicationData: decryptedData.applicationData, + creationDate: response.creationDate, + })), + catchError((error: unknown) => { + // TODO Handle errors appropriately + // console.error("An error occurred when decrypting report", error); + return EMPTY; + }), + ); + }), + catchError((error: unknown) => { + // console.error("An error occurred when fetching the last report", error); + return EMPTY; }), ); } @@ -308,6 +361,7 @@ export class RiskInsightsReportService { saveRiskInsightsReport$( report: ApplicationHealthReportDetail[], summary: OrganizationReportSummary, + applications: OrganizationReportApplication[], encryptionParameters: { organizationId: OrganizationId; userId: UserId; @@ -315,28 +369,43 @@ export class RiskInsightsReportService { ): Observable { return from( this.riskInsightsEncryptionService.encryptRiskInsightsReport( - encryptionParameters.organizationId, - encryptionParameters.userId, { - data: report, - summary: summary, + organizationId: encryptionParameters.organizationId, + userId: encryptionParameters.userId, + }, + { + reportData: report, + summaryData: summary, + applicationData: applications, }, ), ).pipe( - map(({ encryptedData, contentEncryptionKey }) => ({ - data: { - organizationId: encryptionParameters.organizationId, - date: new Date().toISOString(), - reportData: encryptedData.toSdk(), - contentEncryptionKey: contentEncryptionKey.toSdk(), - }, - })), + map( + ({ + encryptedReportData, + encryptedSummaryData, + encryptedApplicationData, + contentEncryptionKey, + }) => ({ + data: { + organizationId: encryptionParameters.organizationId, + creationDate: new Date().toISOString(), + reportData: encryptedReportData.toSdk(), + summaryData: encryptedSummaryData.toSdk(), + applicationData: encryptedApplicationData.toSdk(), + contentEncryptionKey: contentEncryptionKey.toSdk(), + }, + }), + ), switchMap((encryptedReport) => this.riskInsightsApiService.saveRiskInsightsReport$( encryptedReport, encryptionParameters.organizationId, ), ), + catchError((error: unknown) => { + return EMPTY; + }), map((response) => { if (!isSaveRiskInsightsReportResponse(response)) { throw new Error("Invalid response from API"); @@ -367,13 +436,13 @@ export class RiskInsightsReportService { const weakPassword = this.passwordHealthService.findWeakPasswordDetails(cipher); // Looping over all ciphers needs to happen first to determine reused passwords over all ciphers. // Store in the set and evaluate later - if (passwordUseMap.has(cipher.login.password)) { + if (passwordUseMap.has(cipher.login.password!)) { passwordUseMap.set( - cipher.login.password, - (passwordUseMap.get(cipher.login.password) || 0) + 1, + cipher.login.password!, + (passwordUseMap.get(cipher.login.password!) || 0) + 1, ); } else { - passwordUseMap.set(cipher.login.password, 1); + passwordUseMap.set(cipher.login.password!, 1); } const exposedPassword = exposedDetails.find((x) => x?.cipherId === cipher.id); @@ -397,7 +466,7 @@ export class RiskInsightsReportService { // loop for reused passwords cipherHealthReports.forEach((detail) => { - detail.reusedPasswordCount = passwordUseMap.get(detail.login.password) ?? 0; + detail.reusedPasswordCount = passwordUseMap.get(detail.login.password!) ?? 0; }); return cipherHealthReports; } @@ -445,7 +514,7 @@ export class RiskInsightsReportService { private _buildPasswordUseMap(ciphers: CipherView[]): Map { const passwordUseMap = new Map(); ciphers.forEach((cipher) => { - const password = cipher.login.password; + const password = cipher.login.password!; passwordUseMap.set(password, (passwordUseMap.get(password) || 0) + 1); }); return passwordUseMap; @@ -457,6 +526,13 @@ export class RiskInsightsReportService { const applicationMap = new Map(); cipherHealthData.forEach((cipher: CipherHealthReport) => { + // Warning: Currently does not show ciphers with NO Application + // if (cipher.applications.length === 0) { + // const existingApplication = applicationMap.get("None") || []; + // existingApplication.push(cipher); + // applicationMap.set("None", existingApplication); + // } + cipher.applications.forEach((application) => { const existingApplication = applicationMap.get(application) || []; existingApplication.push(cipher); @@ -610,7 +686,7 @@ export class RiskInsightsReportService { healthData: { weakPasswordDetail: this.passwordHealthService.findWeakPasswordDetails(cipher), exposedPasswordDetail: exposedPassword, - reusedPasswordCount: passwordUseMap.get(cipher.login.password) ?? 0, + reusedPasswordCount: passwordUseMap.get(cipher.login.password!) ?? 0, }, applications: getTrimmedCipherUris(cipher), } as CipherHealthReport; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/security-tasks-api.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/security-tasks-api.service.spec.ts new file mode 100644 index 00000000000..63fec460162 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/security-tasks-api.service.spec.ts @@ -0,0 +1,53 @@ +import { mock } from "jest-mock-extended"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; + +import { SecurityTasksApiService, TaskMetrics } from "./security-tasks-api.service"; + +describe("SecurityTasksApiService", () => { + const apiServiceMock = mock(); + let service: SecurityTasksApiService; + + beforeEach(() => { + service = new SecurityTasksApiService(apiServiceMock); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + describe("getTaskMetrics", () => { + it("should call apiService.send with correct parameters", (done) => { + const orgId = { toString: () => "org-123" } as OrganizationId; + const mockMetrics: TaskMetrics = { completedTasks: 2, totalTasks: 5 }; + apiServiceMock.send.mockReturnValue(Promise.resolve(mockMetrics)); + + service.getTaskMetrics(orgId).subscribe((metrics) => { + expect(apiServiceMock.send).toHaveBeenCalledWith( + "GET", + "/tasks/org-123/metrics", + null, + true, + true, + ); + expect(metrics).toEqual(mockMetrics); + done(); + }); + }); + + it("should propagate errors from apiService.send", (done) => { + const orgId = { toString: () => "org-456" } as OrganizationId; + const error = new Error("API error"); + apiServiceMock.send.mockReturnValue(Promise.reject(error)); + + service.getTaskMetrics(orgId).subscribe({ + next: () => {}, + error: (err: unknown) => { + expect(err).toBe(error); + done(); + }, + }); + }); + }); +}); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/security-tasks-api.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/security-tasks-api.service.ts new file mode 100644 index 00000000000..92bb9207453 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/security-tasks-api.service.ts @@ -0,0 +1,25 @@ +import { from, Observable } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; + +export type TaskMetrics = { + completedTasks: number; + totalTasks: number; +}; + +export class SecurityTasksApiService { + constructor(private apiService: ApiService) {} + + getTaskMetrics(orgId: OrganizationId): Observable { + const dbResponse = this.apiService.send( + "GET", + `/tasks/${orgId.toString()}/metrics`, + null, + true, + true, + ); + + return from(dbResponse as Promise); + } +} 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 31e56836375..724e5891dc8 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 @@ -39,12 +39,7 @@ *ngIf="canAccessBilling$ | async" > - @if (managePaymentDetailsOutsideCheckout$ | async) { - - } + ; protected clientsTranslationKey$: Observable; - protected managePaymentDetailsOutsideCheckout$: Observable; protected providerPortalTakeover$: Observable; protected subscriber$: Observable; @@ -100,10 +99,6 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy { ), ); - this.managePaymentDetailsOutsideCheckout$ = this.configService.getFeatureFlag$( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - this.provider$ .pipe( switchMap((provider) => 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 263b90f5b32..24e8a757bdf 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 @@ -7,14 +7,18 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CardComponent, ScrollLayoutDirective, SearchModule } from "@bitwarden/components"; import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component"; import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; -import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component"; -import { VerifyBankAccountComponent } from "@bitwarden/web-vault/app/billing/shared/verify-bank-account/verify-bank-account.component"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, +} from "@bitwarden/web-vault/app/billing/payment/components"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; import { CreateClientDialogComponent, + InvoicesComponent, ManageClientNameDialogComponent, ManageClientSubscriptionDialogComponent, + NoInvoicesComponent, ProviderBillingHistoryComponent, ProviderSubscriptionComponent, ProviderSubscriptionStatusComponent, @@ -52,11 +56,11 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr ProvidersLayoutComponent, DangerZoneComponent, ScrollingModule, - VerifyBankAccountComponent, CardComponent, ScrollLayoutDirective, - PaymentComponent, ProviderWarningsModule, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, ], declarations: [ AcceptProviderComponent, @@ -72,8 +76,10 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr AddEditMemberDialogComponent, AddExistingOrganizationDialogComponent, CreateClientDialogComponent, + InvoicesComponent, ManageClientNameDialogComponent, ManageClientSubscriptionDialogComponent, + NoInvoicesComponent, ProviderBillingHistoryComponent, ProviderSubscriptionComponent, ProviderSubscriptionStatusComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html index daae7e2ed2e..43f49b1fd04 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -29,13 +29,11 @@

{{ "paymentMethod" | i18n }}

- - + + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index 72ca0bc8391..0fa69c7a0e6 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -1,25 +1,24 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, Subject, switchMap } from "rxjs"; import { first, takeUntil } from "rxjs/operators"; -import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ProviderKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, + getBillingAddressFromForm, +} from "@bitwarden/web-vault/app/billing/payment/components"; @Component({ selector: "provider-setup", @@ -27,16 +26,17 @@ import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/paymen standalone: false, }) export class SetupComponent implements OnInit, OnDestroy { - @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; - @ViewChild(ManageTaxInformationComponent) taxInformationComponent: ManageTaxInformationComponent; + @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; loading = true; - providerId: string; - token: string; + providerId!: string; + token!: string; protected formGroup = this.formBuilder.group({ name: ["", Validators.required], billingEmail: ["", [Validators.required, Validators.email]], + paymentMethod: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), }); private destroy$ = new Subject(); @@ -69,7 +69,7 @@ export class SetupComponent implements OnInit, OnDestroy { if (error) { this.toastService.showToast({ variant: "error", - title: null, + title: "", message: this.i18nService.t("emergencyInviteAcceptFailed"), timeout: 10000, }); @@ -95,6 +95,7 @@ export class SetupComponent implements OnInit, OnDestroy { replaceUrl: true, }); } + this.loading = false; } catch (error) { this.validationService.showError(error); @@ -115,10 +116,7 @@ export class SetupComponent implements OnInit, OnDestroy { try { this.formGroup.markAllAsTouched(); - const paymentValid = this.paymentComponent.validate(); - const taxInformationValid = this.taxInformationComponent.validate(); - - if (!paymentValid || !taxInformationValid || !this.formGroup.valid) { + if (this.formGroup.invalid) { return; } const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); @@ -126,29 +124,24 @@ export class SetupComponent implements OnInit, OnDestroy { const key = providerKey[0].encryptedString; const request = new ProviderSetupRequest(); - request.name = this.formGroup.value.name; - request.billingEmail = this.formGroup.value.billingEmail; + request.name = this.formGroup.value.name!; + request.billingEmail = this.formGroup.value.billingEmail!; request.token = this.token; - request.key = key; + request.key = key!; - request.taxInfo = new ExpandedTaxInfoUpdateRequest(); - const taxInformation = this.taxInformationComponent.getTaxInformation(); + const paymentMethod = await this.enterPaymentMethodComponent.tokenize(); + if (!paymentMethod) { + return; + } - request.taxInfo.country = taxInformation.country; - request.taxInfo.postalCode = taxInformation.postalCode; - request.taxInfo.taxId = taxInformation.taxId; - request.taxInfo.line1 = taxInformation.line1; - request.taxInfo.line2 = taxInformation.line2; - request.taxInfo.city = taxInformation.city; - request.taxInfo.state = taxInformation.state; - - request.paymentSource = await this.paymentComponent.tokenize(); + request.paymentMethod = paymentMethod; + request.billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); const provider = await this.providerApiService.postProviderSetup(this.providerId, request); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("providerSetup"), }); @@ -156,20 +149,10 @@ export class SetupComponent implements OnInit, OnDestroy { await this.router.navigate(["/providers", provider.id]); } catch (e) { - if ( - this.paymentComponent.selected === PaymentMethodType.PayPal && - typeof e === "string" && - e === "No payment method is available." - ) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("clickPayWithPayPal"), - }); - } else { + if (e !== null && typeof e === "object" && "message" in e && typeof e.message === "string") { e.message = this.i18nService.translate(e.message) || e.message; - this.validationService.showError(e); } + this.validationService.showError(e); } }; } diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index ef8241b534c..6d2836ee0ba 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -30,8 +30,8 @@ {{ "ssoIdentifier" | i18n }} - {{ "ssoIdentifierHintPartOne" | i18n }} - {{ "claimedDomains" | i18n }} + {{ "ssoIdentifierHint" | i18n }} + {{ "claimedDomainsLearnMore" | i18n }} @@ -209,7 +209,14 @@ {{ "clientSecret" | i18n }} - + + @@ -488,7 +495,6 @@ formControlName="idpSingleSignOnServiceUrl" appInputStripSpaces /> - {{ "idpSingleSignOnServiceUrlRequired" | i18n }} 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 9baeaabb33f..b0f3af4d108 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,7 +9,7 @@ import { Validators, } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, Subject, switchMap, takeUntil } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -34,6 +34,7 @@ import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/or 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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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"; @@ -121,6 +122,8 @@ export class SsoComponent implements OnInit, OnDestroy { spMetadataUrl: string; spAcsUrl: string; + showClientSecret = false; + protected openIdForm = this.formBuilder.group>( { authority: new FormControl("", Validators.required), @@ -156,7 +159,7 @@ export class SsoComponent implements OnInit, OnDestroy { idpEntityId: new FormControl("", Validators.required), idpBindingType: new FormControl(Saml2BindingType.HttpRedirect), - idpSingleSignOnServiceUrl: new FormControl(), + idpSingleSignOnServiceUrl: new FormControl("", Validators.required), idpSingleLogoutServiceUrl: new FormControl(), idpX509PublicCert: new FormControl("", Validators.required), idpOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm), @@ -201,6 +204,7 @@ export class SsoComponent implements OnInit, OnDestroy { private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private toastService: ToastService, + private environmentService: EnvironmentService, ) {} async ngOnInit() { @@ -251,6 +255,32 @@ export class SsoComponent implements OnInit, OnDestroy { .subscribe(); this.showKeyConnectorOptions = this.platformUtilsService.isSelfHost(); + + // Only setup listener if key connector is a possible selection + if (this.showKeyConnectorOptions) { + this.listenForKeyConnectorSelection(); + } + } + + listenForKeyConnectorSelection() { + this.ssoConfigForm?.controls?.memberDecryptionType.valueChanges + .pipe( + switchMap(async (memberDecryptionType) => { + if (memberDecryptionType === MemberDecryptionType.KeyConnector) { + // Pre-populate a default key connector URL (user can still change it) + const env = await firstValueFrom(this.environmentService.environment$); + const webVaultUrl = env.getWebVaultUrl(); + const defaultKeyConnectorUrl = webVaultUrl + "/key-connector/"; + + this.ssoConfigForm.controls.keyConnectorUrl.setValue(defaultKeyConnectorUrl); + } else { + // Otherwise clear the key connector URL + this.ssoConfigForm.controls.keyConnectorUrl.setValue(""); + } + }), + takeUntil(this.destroy$), + ) + .subscribe(); } ngOnDestroy(): void { diff --git a/libs/angular/src/billing/components/invoices/invoices.component.html b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.html similarity index 100% rename from libs/angular/src/billing/components/invoices/invoices.component.html rename to bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.html diff --git a/libs/angular/src/billing/components/invoices/invoices.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.ts similarity index 100% rename from libs/angular/src/billing/components/invoices/invoices.component.ts rename to bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.ts diff --git a/libs/angular/src/billing/components/invoices/no-invoices.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/no-invoices.component.ts similarity index 100% rename from libs/angular/src/billing/components/invoices/no-invoices.component.ts rename to bitwarden_license/bit-web/src/app/billing/providers/billing-history/no-invoices.component.ts diff --git a/bitwarden_license/bit-web/src/app/billing/providers/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/index.ts index b1294bc8047..3cd83e68990 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/index.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/index.ts @@ -1,3 +1,5 @@ +export * from "./billing-history/invoices.component"; +export * from "./billing-history/no-invoices.component"; export * from "./billing-history/provider-billing-history.component"; export * from "./clients"; export * from "./guards/has-consolidated-billing.guard"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts index d2ac2cede2f..5a070687de4 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts @@ -1,13 +1,10 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute } from "@angular/router"; import { BehaviorSubject, combineLatest, - EMPTY, filter, firstValueFrom, - from, - map, merge, Observable, of, @@ -19,7 +16,6 @@ import { tap, withLatestFrom, } from "rxjs"; -import { catchError } from "rxjs/operators"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; @@ -49,13 +45,6 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { ProviderWarningsService } from "../warnings/services"; -class RedirectError { - constructor( - public path: string[], - public relativeTo: ActivatedRoute, - ) {} -} - type View = { activeUserId: UserId; provider: BitwardenSubscriber; @@ -92,18 +81,6 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy { ); private load$: Observable = this.provider$.pipe( - switchMap((provider) => - this.configService - .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) - .pipe( - map((managePaymentDetailsOutsideCheckout) => { - if (!managePaymentDetailsOutsideCheckout) { - throw new RedirectError(["../subscription"], this.activatedRoute); - } - return provider; - }), - ), - ), mapProviderToSubscriber, switchMap(async (provider) => { const getTaxIdWarning = firstValueFrom( @@ -131,14 +108,6 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy { }; }), shareReplay({ bufferSize: 1, refCount: false }), - catchError((error: unknown) => { - if (error instanceof RedirectError) { - return from(this.router.navigate(error.path, { relativeTo: error.relativeTo })).pipe( - switchMap(() => EMPTY), - ); - } - throw error; - }), ); view$: Observable = merge( @@ -158,7 +127,6 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy { private messageListener: MessageListener, private providerService: ProviderService, private providerWarningsService: ProviderWarningsService, - private router: Router, private subscriberBillingClient: SubscriberBillingClient, ) {} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html index 0205d2838d1..05eda7e7ea4 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html @@ -62,51 +62,5 @@
- @if (!managePaymentDetailsOutsideCheckout) { - - -

- {{ "accountCredit" | i18n }} -

-

{{ subscription.accountCredit | currency: "$" }}

-

{{ "creditAppliedDesc" | i18n }}

-
- - -

{{ "paymentMethod" | i18n }}

-

- {{ "noPaymentMethod" | i18n }} -

- - - -

- - {{ subscription.paymentSource.description }} - - {{ "unverified" | i18n }} -

-
- -
- - -

{{ "taxInformation" | i18n }}

-

{{ "taxInformationDesc" | i18n }}

- -
- } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts index 83a23760d80..98aceb0f878 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts @@ -2,26 +2,14 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, lastValueFrom, Subject, takeUntil } from "rxjs"; +import { concatMap, Subject, takeUntil } from "rxjs"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; -import { PaymentMethodType } 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 { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; import { ProviderPlanResponse, ProviderSubscriptionResponse, } from "@bitwarden/common/billing/models/response/provider-subscription-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 { DialogService, ToastService } from "@bitwarden/components"; import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service"; -import { - AdjustPaymentDialogComponent, - AdjustPaymentDialogResultType, -} from "@bitwarden/web-vault/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component"; @Component({ selector: "app-provider-subscription", @@ -36,18 +24,11 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { protected loading: boolean; private destroy$ = new Subject(); protected totalCost: number; - protected managePaymentDetailsOutsideCheckout: boolean; - - protected readonly TaxInformation = TaxInformation; constructor( private billingApiService: BillingApiServiceAbstraction, - private i18nService: I18nService, private route: ActivatedRoute, private billingNotificationService: BillingNotificationService, - private dialogService: DialogService, - private toastService: ToastService, - private configService: ConfigService, ) {} async ngOnInit() { @@ -55,9 +36,6 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { .pipe( concatMap(async (params) => { this.providerId = params.providerId; - this.managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); await this.load(); this.firstLoaded = true; }), @@ -83,40 +61,6 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { } } - protected updatePaymentMethod = async (): Promise => { - const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { - data: { - initialPaymentMethod: this.subscription.paymentSource?.type, - providerId: this.providerId, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result === AdjustPaymentDialogResultType.Submitted) { - await this.load(); - } - }; - - protected updateTaxInformation = async (taxInformation: TaxInformation) => { - try { - const request = ExpandedTaxInfoUpdateRequest.From(taxInformation); - await this.billingApiService.updateProviderTaxInformation(this.providerId, request); - this.billingNotificationService.showSuccess(this.i18nService.t("updatedTaxInformation")); - } catch (error) { - this.billingNotificationService.handleError(error); - } - }; - - protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise => { - await this.billingApiService.verifyProviderBankAccount(this.providerId, request); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("verifiedBankAccount"), - }); - }; - protected getFormattedCost( cost: number, seatMinimum: number, @@ -161,7 +105,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { } protected getBillingCadenceLabel(providerPlanResponse: ProviderPlanResponse): string { - if (providerPlanResponse == null || providerPlanResponse == undefined) { + if (providerPlanResponse == null) { return "month"; } @@ -174,27 +118,4 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { return "month"; } } - - protected get paymentSourceClasses() { - if (this.subscription.paymentSource == null) { - return []; - } - switch (this.subscription.paymentSource.type) { - case PaymentMethodType.Card: - return ["bwi-credit-card"]; - case PaymentMethodType.BankAccount: - case PaymentMethodType.Check: - return ["bwi-billing"]; - case PaymentMethodType.PayPal: - return ["bwi-paypal text-primary"]; - default: - return []; - } - } - - protected get updatePaymentSourceButtonText(): string { - const key = - this.subscription.paymentSource == null ? "addPaymentMethod" : "changePaymentMethod"; - return this.i18nService.t(key); - } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/warnings/types/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/warnings/types/index.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts index 6848220446b..657f76d414b 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts @@ -10,6 +10,7 @@ import { RiskInsightsApiService, RiskInsightsDataService, RiskInsightsReportService, + SecurityTasksApiService, } from "@bitwarden/bit-common/dirt/reports/risk-insights/services"; import { RiskInsightsEncryptionService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/risk-insights-encryption.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -28,28 +29,32 @@ import { RiskInsightsComponent } from "./risk-insights.component"; @NgModule({ imports: [RiskInsightsComponent, AccessIntelligenceRoutingModule], providers: [ - { + safeProvider({ provide: MemberCipherDetailsApiService, + useClass: MemberCipherDetailsApiService, deps: [ApiService], - }, - { + }), + safeProvider({ provide: PasswordHealthService, + useClass: PasswordHealthService, deps: [PasswordStrengthServiceAbstraction, AuditService], - }, - { + }), + safeProvider({ provide: RiskInsightsApiService, + useClass: RiskInsightsApiService, deps: [ApiService], - }, - { + }), + safeProvider({ provide: RiskInsightsReportService, + useClass: RiskInsightsReportService, deps: [ CipherService, MemberCipherDetailsApiService, + PasswordHealthService, RiskInsightsApiService, RiskInsightsEncryptionService, - PasswordHealthService, ], - }, + }), safeProvider({ provide: RiskInsightsDataService, deps: [ @@ -77,7 +82,12 @@ import { RiskInsightsComponent } from "./risk-insights.component"; safeProvider({ provide: AllActivitiesService, useClass: AllActivitiesService, - deps: [], + deps: [RiskInsightsDataService], + }), + safeProvider({ + provide: SecurityTasksApiService, + useClass: SecurityTasksApiService, + deps: [ApiService], }), ], }) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.html index 17ae964dbed..0eb9b30367c 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.html @@ -1,12 +1,29 @@
{{ title }}
+ @if (iconClass) { + + } {{ cardMetrics }}
{{ metricDescription }}
- @if (showNavigationLink) { + @if (buttonClick.observed && buttonText) { +
+ +
+ } + @if (showNavigationLink && !buttonText) {

diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.ts index 7de339358f3..c8c73cd0e5a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-card.component.ts @@ -1,9 +1,9 @@ import { CommonModule } from "@angular/common"; -import { Component, Input } from "@angular/core"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components"; +import { ButtonModule, ButtonType, LinkModule, TypographyModule } from "@bitwarden/components"; @Component({ selector: "dirt-activity-card", @@ -11,7 +11,7 @@ import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/component imports: [CommonModule, TypographyModule, JslibModule, LinkModule, ButtonModule], host: { class: - "tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-rounded-lg tw-p-6", + "tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-rounded-lg tw-p-6 tw-h-56 tw-max-h-56", }, }) export class ActivityCardComponent { @@ -43,9 +43,34 @@ export class ActivityCardComponent { */ @Input() showNavigationLink: boolean = false; + /** + * Icon class to display next to metrics (e.g., "bwi-exclamation-triangle"). + * If null, no icon is displayed. + */ + @Input() iconClass: string | null = null; + + /** + * Button text. If provided, a button will be displayed instead of a navigation link. + */ + @Input() buttonText: string = ""; + + /** + * Button type (e.g., "primary", "secondary") + */ + @Input() buttonType: ButtonType = "primary"; + + /** + * Event emitted when button is clicked + */ + @Output() buttonClick = new EventEmitter(); + constructor(private router: Router) {} navigateToLink = async (navigationLink: string) => { await this.router.navigateByUrl(navigationLink); }; + + onButtonClick = () => { + this.buttonClick.emit(); + }; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html new file mode 100644 index 00000000000..9b194954f0e --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html @@ -0,0 +1,75 @@ +

diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts new file mode 100644 index 00000000000..b7a36a79988 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts @@ -0,0 +1,204 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { Subject, switchMap, takeUntil, of, BehaviorSubject, combineLatest } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + AllActivitiesService, + LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, + SecurityTasksApiService, + TaskMetrics, +} from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; +import { SecurityTaskType } from "@bitwarden/common/vault/tasks"; +import { + ButtonModule, + ProgressModule, + ToastService, + TypographyModule, +} from "@bitwarden/components"; + +import { CreateTasksRequest } from "../../../vault/services/abstractions/admin-task.abstraction"; +import { DefaultAdminTaskService } from "../../../vault/services/default-admin-task.service"; + +export const RenderMode = { + noCriticalApps: "noCriticalApps", + criticalAppsWithAtRiskAppsAndNoTasks: "criticalAppsWithAtRiskAppsAndNoTasks", + criticalAppsWithAtRiskAppsAndTasks: "criticalAppsWithAtRiskAppsAndTasks", +} as const; +export type RenderMode = (typeof RenderMode)[keyof typeof RenderMode]; + +@Component({ + selector: "dirt-password-change-metric", + imports: [CommonModule, TypographyModule, JslibModule, ProgressModule, ButtonModule], + templateUrl: "./password-change-metric.component.html", + providers: [DefaultAdminTaskService], +}) +export class PasswordChangeMetricComponent implements OnInit { + protected taskMetrics$ = new BehaviorSubject({ totalTasks: 0, completedTasks: 0 }); + private completedTasks: number = 0; + private totalTasks: number = 0; + private allApplicationsDetails: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[] = + []; + + atRiskAppsCount: number = 0; + atRiskPasswordsCount: number = 0; + private organizationId!: OrganizationId; + private destroyRef = new Subject(); + renderMode: RenderMode = "noCriticalApps"; + + async ngOnInit(): Promise { + this.activatedRoute.paramMap + .pipe( + switchMap((paramMap) => { + const orgId = paramMap.get("organizationId"); + if (orgId) { + this.organizationId = orgId as OrganizationId; + return this.securityTasksApiService.getTaskMetrics(this.organizationId); + } + return of({ totalTasks: 0, completedTasks: 0 }); + }), + takeUntil(this.destroyRef), + ) + .subscribe((metrics) => { + this.taskMetrics$.next(metrics); + }); + + combineLatest([ + this.taskMetrics$, + this.allActivitiesService.reportSummary$, + this.allActivitiesService.atRiskPasswordsCount$, + this.allActivitiesService.allApplicationsDetails$, + ]) + .pipe(takeUntil(this.destroyRef)) + .subscribe(([taskMetrics, summary, atRiskPasswordsCount, allApplicationsDetails]) => { + this.atRiskAppsCount = summary.totalCriticalAtRiskApplicationCount; + this.atRiskPasswordsCount = atRiskPasswordsCount; + this.completedTasks = taskMetrics.completedTasks; + this.totalTasks = taskMetrics.totalTasks; + this.allApplicationsDetails = allApplicationsDetails; + + // No critical apps setup + this.renderMode = + summary.totalCriticalApplicationCount === 0 ? RenderMode.noCriticalApps : this.renderMode; + + // Critical apps setup with at-risk apps but no tasks + this.renderMode = + summary.totalCriticalApplicationCount > 0 && + summary.totalCriticalAtRiskApplicationCount >= 0 && + taskMetrics.totalTasks === 0 + ? RenderMode.criticalAppsWithAtRiskAppsAndNoTasks + : this.renderMode; + + // Critical apps setup with at-risk apps and tasks + this.renderMode = + summary.totalAtRiskApplicationCount > 0 && + summary.totalCriticalAtRiskApplicationCount >= 0 && + taskMetrics.totalTasks > 0 + ? RenderMode.criticalAppsWithAtRiskAppsAndTasks + : this.renderMode; + + this.allActivitiesService.setPasswordChangeProgressMetricHasProgressBar( + this.renderMode === RenderMode.criticalAppsWithAtRiskAppsAndTasks, + ); + }); + } + + constructor( + private activatedRoute: ActivatedRoute, + private securityTasksApiService: SecurityTasksApiService, + private allActivitiesService: AllActivitiesService, + private adminTaskService: DefaultAdminTaskService, + protected toastService: ToastService, + protected i18nService: I18nService, + ) {} + + get completedPercent(): number { + if (this.totalTasks === 0) { + return 0; + } + return Math.round((this.completedTasks / this.totalTasks) * 100); + } + + get completedTasksCount(): number { + switch (this.renderMode) { + case RenderMode.noCriticalApps: + case RenderMode.criticalAppsWithAtRiskAppsAndNoTasks: + return 0; + + case RenderMode.criticalAppsWithAtRiskAppsAndTasks: + return this.completedTasks; + + default: + return 0; + } + } + + get totalTasksCount(): number { + switch (this.renderMode) { + case RenderMode.noCriticalApps: + return 0; + + case RenderMode.criticalAppsWithAtRiskAppsAndNoTasks: + return this.atRiskAppsCount; + + case RenderMode.criticalAppsWithAtRiskAppsAndTasks: + return this.totalTasks; + + default: + return 0; + } + } + + get canAssignTasks(): boolean { + return this.atRiskAppsCount > this.totalTasks ? true : false; + } + + get renderModes() { + return RenderMode; + } + + async assignTasks() { + const taskCount = await this.requestPasswordChange(); + this.taskMetrics$.next({ + totalTasks: this.totalTasks + taskCount, + completedTasks: this.completedTasks, + }); + } + + // TODO: this method is shared between here and critical-applications.component.ts + async requestPasswordChange() { + const apps = this.allApplicationsDetails; + const cipherIds = apps + .filter((_) => _.atRiskPasswordCount > 0) + .flatMap((app) => app.atRiskCipherIds); + + const distinctCipherIds = Array.from(new Set(cipherIds)); + + const tasks: CreateTasksRequest[] = distinctCipherIds.map((cipherId) => ({ + cipherId: cipherId as CipherId, + type: SecurityTaskType.UpdateAtRiskCredential, + })); + + try { + await this.adminTaskService.bulkCreateTasks(this.organizationId as OrganizationId, tasks); + this.toastService.showToast({ + message: this.i18nService.t("notifiedMembers"), + variant: "success", + title: this.i18nService.t("success"), + }); + + return tasks.length; + } catch { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + } + + return 0; + } +} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html index 6598d197172..844b2f92bb3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.html @@ -1,47 +1,37 @@ -@if (isLoading$ | async) { -
- -
-} +@if (dataService.isLoading$ | async) { + +} @else { +
    +
  • + +
  • -@if (!(isLoading$ | async) && (noData$ | async)) { -
    - - -

    - {{ "noAppsInOrgTitle" | i18n: organization?.name }} -

    -
    -
    -
    -} - -@if (!(isLoading$ | async) && !(noData$ | async)) { -
    -
    +
  • +
  • +
  • -
  • -
    + + +
  • + + +
  • +
} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.ts index e69dc2b06e5..31ab0351162 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-activity.component.ts @@ -1,7 +1,7 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute } from "@angular/router"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { AllActivitiesService, @@ -11,25 +11,33 @@ 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { getById } from "@bitwarden/common/platform/misc"; +import { ToastService } from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { ActivityCardComponent } from "./activity-card.component"; +import { PasswordChangeMetricComponent } from "./activity-cards/password-change-metric.component"; import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"; import { RiskInsightsTabType } from "./risk-insights.component"; @Component({ - selector: "tools-all-activity", - imports: [ApplicationsLoadingComponent, SharedModule, ActivityCardComponent], + selector: "dirt-all-activity", + imports: [ + ApplicationsLoadingComponent, + SharedModule, + ActivityCardComponent, + PasswordChangeMetricComponent, + ], templateUrl: "./all-activity.component.html", }) export class AllActivityComponent implements OnInit { - protected isLoading$ = this.dataService.isLoading$; - protected noData$ = new BehaviorSubject(true); organization: Organization | null = null; totalCriticalAppsAtRiskMemberCount = 0; totalCriticalAppsCount = 0; totalCriticalAppsAtRiskCount = 0; + newApplicationsCount = 0; + passwordChangeMetricHasProgressBar = false; destroyRef = inject(DestroyRef); @@ -46,10 +54,16 @@ export class AllActivityComponent implements OnInit { this.allActivitiesService.reportSummary$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((summary) => { - this.noData$.next(summary.totalApplicationCount === 0); this.totalCriticalAppsAtRiskMemberCount = summary.totalCriticalAtRiskMemberCount; this.totalCriticalAppsCount = summary.totalCriticalApplicationCount; this.totalCriticalAppsAtRiskCount = summary.totalCriticalAtRiskApplicationCount; + this.newApplicationsCount = summary.newApplications.length; + }); + + this.allActivitiesService.passwordChangeProgressMetricHasProgressBar$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((hasProgressBar) => { + this.passwordChangeMetricHasProgressBar = hasProgressBar; }); } } @@ -60,6 +74,8 @@ export class AllActivityComponent implements OnInit { protected organizationService: OrganizationService, protected dataService: RiskInsightsDataService, protected allActivitiesService: AllActivitiesService, + private toastService: ToastService, + private i18nService: I18nService, ) {} get RiskInsightsTabType() { @@ -70,4 +86,17 @@ export class AllActivityComponent implements OnInit { const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId"); return `/organizations/${organizationId}/access-intelligence/risk-insights?tabIndex=${tabIndex}`; } + + /** + * Handles the review new applications button click. + * Shows a toast notification as a placeholder until the dialog is implemented. + * TODO: Implement dialog for reviewing new applications (follow-up task) + */ + onReviewNewApplications = () => { + this.toastService.showToast({ + variant: "info", + title: this.i18nService.t("applicationsNeedingReview"), + message: this.i18nService.t("newApplicationsWithCount", this.newApplicationsCount.toString()), + }); + }; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html index febdb5fa0de..1971b61d516 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html @@ -1,96 +1,102 @@ -
- -
-
- - -

- {{ "noAppsInOrgTitle" | i18n: organization?.name }} -

-
- -
- - {{ "noAppsInOrgDescription" | i18n }} - - {{ "learnMore" | i18n }} +@if (dataService.isLoading$ | async) { + +} @else { + @let drawerDetails = dataService.drawerDetails$ | async; + @if (!dataSource.data.length) { +
+ + +

+ {{ + "noAppsInOrgTitle" + | i18n: (dataService.organizationDetails$ | async)?.organizationName || "" + }} +

+
+ +
+ + {{ "noAppsInOrgDescription" | i18n }} + + {{ "learnMore" | i18n }} +
+
+ + + +
+
+ } @else { +
+

{{ "allApplications" | i18n }}

+
+ +
- - - - - -
-
-

{{ "allApplications" | i18n }}

- @if (dataService.drawerDetails$ | async; as drawerDetails) { -
- - -
-
- - -
+ + {{ "markAppAsCritical" | i18n }} + +
- + +
} -
+} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts index 3b7490dbc19..bc04884c799 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts @@ -2,33 +2,17 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, debounceTime, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; +import { debounceTime } from "rxjs"; import { Security } from "@bitwarden/assets/svg"; import { - AllActivitiesService, - CriticalAppsService, + ApplicationHealthReportDetailEnriched, RiskInsightsDataService, - RiskInsightsReportService, } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers"; -import { - LEGACY_ApplicationHealthReportDetailWithCriticalFlag, - LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, -} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; import { OrganizationReportSummary } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models"; -import { RiskInsightsEncryptionService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/risk-insights-encryption.service"; -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 { 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { IconButtonModule, NoItemsModule, @@ -45,7 +29,7 @@ import { AppTableRowScrollableComponent } from "./app-table-row-scrollable.compo import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"; @Component({ - selector: "tools-all-applications", + selector: "dirt-all-applications", templateUrl: "./all-applications.component.html", imports: [ ApplicationsLoadingComponent, @@ -60,97 +44,44 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component" ], }) export class AllApplicationsComponent implements OnInit { - protected dataSource = - new TableDataSource(); + protected dataSource = new TableDataSource(); protected selectedUrls: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); - protected loading = true; protected organization = new Organization(); noItemsIcon = Security; protected markingAsCritical = false; protected applicationSummary: OrganizationReportSummary = createNewSummaryData(); destroyRef = inject(DestroyRef); - isLoading$: Observable = of(false); - - async ngOnInit() { - const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId"); - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - - if (organizationId) { - const organization$ = this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(organizationId)); - - combineLatest([ - this.dataService.applications$, - this.criticalAppsService.getAppsListForOrg(organizationId as OrganizationId), - organization$, - ]) - .pipe( - takeUntilDestroyed(this.destroyRef), - map(([applications, criticalApps, organization]) => { - if (applications && applications.length === 0 && criticalApps && criticalApps) { - const criticalUrls = criticalApps.map((ca) => ca.uri); - const data = applications?.map((app) => ({ - ...app, - isMarkedAsCritical: criticalUrls.includes(app.applicationName), - })) as LEGACY_ApplicationHealthReportDetailWithCriticalFlag[]; - return { data, organization }; - } - - return { data: applications, organization }; - }), - switchMap(async ({ data, organization }) => { - if (data && organization) { - const dataWithCiphers = await this.reportService.identifyCiphers( - data, - organization.id as OrganizationId, - ); - - return { - data: dataWithCiphers, - organization, - }; - } - - return { data: [], organization }; - }), - ) - .subscribe(({ data, organization }) => { - if (data) { - this.dataSource.data = data; - this.applicationSummary = this.reportService.generateApplicationsSummary(data); - this.allActivitiesService.setAllAppsReportSummary(this.applicationSummary); - } - if (organization) { - this.organization = organization; - } - }); - - this.isLoading$ = this.dataService.isLoading$; - } - } constructor( - protected cipherService: CipherService, protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, protected toastService: ToastService, - protected configService: ConfigService, protected dataService: RiskInsightsDataService, - protected organizationService: OrganizationService, - protected reportService: RiskInsightsReportService, - private accountService: AccountService, - protected criticalAppsService: CriticalAppsService, - protected riskInsightsEncryptionService: RiskInsightsEncryptionService, - protected allActivitiesService: AllActivitiesService, + // protected allActivitiesService: AllActivitiesService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) .subscribe((v) => (this.dataSource.filter = v)); } + async ngOnInit() { + this.dataService.reportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ + next: (report) => { + this.applicationSummary = report?.summaryData ?? createNewSummaryData(); + this.dataSource.data = report?.reportData ?? []; + }, + error: () => { + this.dataSource.data = []; + }, + }); + + // TODO + // this.applicationSummary = this.reportService.generateApplicationsSummary(data); + // this.allActivitiesService.setAllAppsReportSummary(this.applicationSummary); + } + goToCreateNewLoginItem = async () => { // TODO: implement this.toastService.showToast({ @@ -167,41 +98,31 @@ export class AllApplicationsComponent implements OnInit { markAppsAsCritical = async () => { this.markingAsCritical = true; - try { - await this.criticalAppsService.setCriticalApps( - this.organization.id as OrganizationId, - Array.from(this.selectedUrls), - ); - - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("applicationsMarkedAsCriticalSuccess"), + this.dataService + .saveCriticalApplications(Array.from(this.selectedUrls)) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("applicationsMarkedAsCriticalSuccess"), + }); + this.selectedUrls.clear(); + this.markingAsCritical = false; + }, + error: () => { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("applicationsMarkedAsCriticalFail"), + }); + }, }); - } finally { - this.selectedUrls.clear(); - this.markingAsCritical = false; - } }; showAppAtRiskMembers = async (applicationName: string) => { - const info = { - members: - this.dataSource.data.find((app) => app.applicationName === applicationName) - ?.atRiskMemberDetails ?? [], - applicationName, - }; - this.dataService.setDrawerForAppAtRiskMembers(info, applicationName); - }; - - showOrgAtRiskMembers = async (invokerId: string) => { - const dialogData = this.reportService.generateAtRiskMemberList(this.dataSource.data); - this.dataService.setDrawerForOrgAtRiskMembers(dialogData, invokerId); - }; - - showOrgAtRiskApps = async (invokerId: string) => { - const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data); - this.dataService.setDrawerForOrgAtRiskApps(data, invokerId); + await this.dataService.setDrawerForAppAtRiskMembers(applicationName); }; onCheckboxChange = (applicationName: string, event: Event) => { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts index 01f3b8fb494..e34b13176ee 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts @@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; +import { ApplicationHealthReportDetailEnriched } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { MenuModule, TableDataSource, TableModule } from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; @@ -14,7 +14,7 @@ import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pip }) export class AppTableRowScrollableComponent { @Input() - dataSource!: TableDataSource; + dataSource!: TableDataSource; @Input() showRowMenuForCriticalApps: boolean = false; @Input() showRowCheckBox: boolean = false; @Input() selectedUrls: Set = new Set(); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html index ed2a6b96524..cfcdf3a1841 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html @@ -49,7 +49,7 @@ type="button" class="tw-flex-1" tabindex="0" - (click)="showOrgAtRiskMembers('criticalAppsAtRiskMembers')" + (click)="dataService.setDrawerForOrgAtRiskMembers('criticalAppsAtRiskMembers')" > }
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts index f092b1575f0..6a0ac5c2883 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts @@ -4,23 +4,15 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, debounceTime, firstValueFrom, map, switchMap } from "rxjs"; +import { debounceTime, EMPTY, map, switchMap } from "rxjs"; import { Security } from "@bitwarden/assets/svg"; import { - AllActivitiesService, - CriticalAppsService, + ApplicationHealthReportDetailEnriched, RiskInsightsDataService, - RiskInsightsReportService, } from "@bitwarden/bit-common/dirt/reports/risk-insights"; -import { - LEGACY_ApplicationHealthReportDetailWithCriticalFlag, - LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, -} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; +import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers"; import { OrganizationReportSummary } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models"; -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 { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; import { SecurityTaskType } from "@bitwarden/common/vault/tasks"; @@ -37,7 +29,7 @@ import { AppTableRowScrollableComponent } from "./app-table-row-scrollable.compo import { RiskInsightsTabType } from "./risk-insights.component"; @Component({ - selector: "tools-critical-applications", + selector: "dirt-critical-applications", templateUrl: "./critical-applications.component.html", imports: [ CardComponent, @@ -51,59 +43,57 @@ import { RiskInsightsTabType } from "./risk-insights.component"; providers: [DefaultAdminTaskService], }) export class CriticalApplicationsComponent implements OnInit { - protected dataSource = - new TableDataSource(); - protected selectedIds: Set = new Set(); - protected searchControl = new FormControl("", { nonNullable: true }); private destroyRef = inject(DestroyRef); protected loading = false; + protected enableRequestPasswordChange = false; protected organizationId: OrganizationId; - protected applicationSummary = {} as OrganizationReportSummary; noItemsIcon = Security; - enableRequestPasswordChange = false; + + protected dataSource = new TableDataSource(); + protected applicationSummary = {} as OrganizationReportSummary; + + protected selectedIds: Set = new Set(); + protected searchControl = new FormControl("", { nonNullable: true }); + + constructor( + protected activatedRoute: ActivatedRoute, + protected router: Router, + protected toastService: ToastService, + protected dataService: RiskInsightsDataService, + protected i18nService: I18nService, + private adminTaskService: DefaultAdminTaskService, + ) { + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((v) => (this.dataSource.filter = v)); + } async ngOnInit() { - this.organizationId = this.activatedRoute.snapshot.paramMap.get( - "organizationId", - ) as OrganizationId; - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - this.criticalAppsService.loadOrganizationContext(this.organizationId as OrganizationId, userId); - - if (this.organizationId) { - combineLatest([ - this.dataService.applications$, - this.criticalAppsService.getAppsListForOrg(this.organizationId as OrganizationId), - ]) - .pipe( - takeUntilDestroyed(this.destroyRef), - map(([applications, criticalApps]) => { - const criticalUrls = criticalApps.map((ca) => ca.uri); - const data = applications?.map((app) => ({ - ...app, - isMarkedAsCritical: criticalUrls.includes(app.applicationName), - })) as LEGACY_ApplicationHealthReportDetailWithCriticalFlag[]; - return data?.filter((app) => app.isMarkedAsCritical); - }), - switchMap(async (data) => { - if (data) { - const dataWithCiphers = await this.reportService.identifyCiphers( - data, - this.organizationId, - ); - return dataWithCiphers; - } - return null; - }), - ) - .subscribe((applications) => { - if (applications) { - this.dataSource.data = applications; - this.applicationSummary = this.reportService.generateApplicationsSummary(applications); - this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0; - this.allActivitiesService.setCriticalAppsReportSummary(this.applicationSummary); + this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ + next: (criticalReport) => { + this.dataSource.data = criticalReport?.reportData ?? []; + this.applicationSummary = criticalReport?.summaryData ?? createNewSummaryData(); + this.enableRequestPasswordChange = criticalReport?.summaryData?.totalAtRiskMemberCount > 0; + }, + error: () => { + this.dataSource.data = []; + this.applicationSummary = createNewSummaryData(); + this.enableRequestPasswordChange = false; + }, + }); + this.activatedRoute.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map((params) => params.get("organizationId")), + switchMap(async (orgId) => { + if (orgId) { + this.organizationId = orgId as OrganizationId; + } else { + return EMPTY; } - }); - } + }), + ) + .subscribe(); } goToAllAppsTab = async () => { @@ -116,26 +106,25 @@ export class CriticalApplicationsComponent implements OnInit { ); }; - unmarkAsCritical = 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"), + removeCriticalApplication = async (hostname: string) => { + this.dataService + .removeCriticalApplication(hostname) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.toastService.showToast({ + message: this.i18nService.t("criticalApplicationUnmarkedSuccessfully"), + variant: "success", + }); + }, + error: () => { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + }, }); - return; - } - - this.toastService.showToast({ - message: this.i18nService.t("criticalApplicationUnmarkedSuccessfully"), - variant: "success", - }); - this.dataSource.data = this.dataSource.data.filter((app) => app.applicationName !== hostname); }; async requestPasswordChange() { @@ -166,42 +155,7 @@ export class CriticalApplicationsComponent implements OnInit { }); } } - - constructor( - protected activatedRoute: ActivatedRoute, - protected router: Router, - protected toastService: ToastService, - protected dataService: RiskInsightsDataService, - protected criticalAppsService: CriticalAppsService, - protected reportService: RiskInsightsReportService, - protected i18nService: I18nService, - private configService: ConfigService, - private adminTaskService: DefaultAdminTaskService, - private accountService: AccountService, - private allActivitiesService: AllActivitiesService, - ) { - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((v) => (this.dataSource.filter = v)); - } - showAppAtRiskMembers = async (applicationName: string) => { - const data = { - members: - this.dataSource.data.find((app) => app.applicationName === applicationName) - ?.atRiskMemberDetails ?? [], - applicationName, - }; - this.dataService.setDrawerForAppAtRiskMembers(data, applicationName); - }; - - showOrgAtRiskMembers = async (invokerId: string) => { - const data = this.reportService.generateAtRiskMemberList(this.dataSource.data); - this.dataService.setDrawerForOrgAtRiskMembers(data, invokerId); - }; - - showOrgAtRiskApps = async (invokerId: string) => { - const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data); - this.dataService.setDrawerForOrgAtRiskApps(data, invokerId); + await this.dataService.setDrawerForAppAtRiskMembers(applicationName); }; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights-loading.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights-loading.component.ts index af61c9a35c8..1d18ca3a030 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights-loading.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights-loading.component.ts @@ -4,7 +4,7 @@ import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @Component({ - selector: "tools-risk-insights-loading", + selector: "dirt-risk-insights-loading", imports: [CommonModule, JslibModule], templateUrl: "./risk-insights-loading.component.html", }) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index 50af2c9e9a7..49ccfb73c5d 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -4,19 +4,23 @@ {{ "reviewAtRiskPasswords" | i18n }}
- {{ - "dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a") - }} - + @if (dataLastUpdated) { + {{ + "dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a") + }} + } @else { + {{ "noReportRan" | i18n }} + } + @let isRunningReport = dataService.isRunningReport$ | async; + @@ -38,18 +42,21 @@ @if (isRiskInsightsActivityTabFeatureEnabled) { - + } - + - {{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }} + {{ + "criticalApplicationsWithCount" + | i18n: (dataService.criticalReportResults$ | async)?.reportData?.length ?? 0 + }} - + @@ -69,7 +76,9 @@ }}
-
{{ "email" | i18n }}
+
+ {{ "email" | i18n }} +
{{ "atRiskPasswords" | i18n }}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index 208ba59fb9d..308cc351dc3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -2,21 +2,12 @@ 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 { EMPTY, firstValueFrom, Observable } from "rxjs"; +import { EMPTY } from "rxjs"; import { map, switchMap } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - CriticalAppsService, - RiskInsightsDataService, -} from "@bitwarden/bit-common/dirt/reports/risk-insights"; -import { PasswordHealthReportApplicationsResponse } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/api-models.types"; -import { - ApplicationHealthReportDetail, - DrawerType, -} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { DrawerType } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; @@ -67,19 +58,13 @@ export class RiskInsightsComponent implements OnInit { tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllApps; isRiskInsightsActivityTabFeatureEnabled: boolean = false; - dataLastUpdated: Date = new Date(); - - criticalApps$: Observable = new Observable(); - appsCount: number = 0; - criticalAppsCount: number = 0; - notifiedMembersCount: number = 0; + // Leaving this commented because it's not used but seems important + // notifiedMembersCount: number = 0; private organizationId: OrganizationId = "" as OrganizationId; - isLoading$: Observable = new Observable(); - isRefreshing$: Observable = new Observable(); - dataLastUpdated$: Observable = new Observable(); + dataLastUpdated: Date | null = null; refetching: boolean = false; constructor( @@ -87,8 +72,6 @@ export class RiskInsightsComponent implements OnInit { private router: Router, private configService: ConfigService, protected dataService: RiskInsightsDataService, - private criticalAppsService: CriticalAppsService, - private accountService: AccountService, ) { this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps; @@ -104,39 +87,29 @@ export class RiskInsightsComponent implements OnInit { } async ngOnInit() { - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - this.route.paramMap .pipe( takeUntilDestroyed(this.destroyRef), map((params) => params.get("organizationId")), - switchMap((orgId) => { + switchMap(async (orgId) => { if (orgId) { + // Initialize Data Service + await this.dataService.initializeForOrganization(orgId as OrganizationId); + this.organizationId = orgId as OrganizationId; - this.dataService.fetchApplicationsReport(this.organizationId); - this.isLoading$ = this.dataService.isLoading$; - this.isRefreshing$ = this.dataService.isRefreshing$; - this.dataLastUpdated$ = this.dataService.dataLastUpdated$; - return this.dataService.applications$; } else { return EMPTY; } }), ) - .subscribe({ - next: (applications: ApplicationHealthReportDetail[] | null) => { - if (applications) { - this.appsCount = applications.length; - } + .subscribe(); - this.criticalAppsService.loadOrganizationContext( - this.organizationId as OrganizationId, - userId, - ); - this.criticalApps$ = this.criticalAppsService.getAppsListForOrg( - this.organizationId as OrganizationId, - ); - }, + // Subscribe to report result details + this.dataService.reportResults$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((report) => { + this.appsCount = report?.reportData.length ?? 0; + this.dataLastUpdated = report?.creationDate ?? null; }); // Subscribe to drawer state changes @@ -156,7 +129,7 @@ export class RiskInsightsComponent implements OnInit { */ refreshData(): void { if (this.organizationId) { - this.dataService.refreshApplicationsReport(this.organizationId); + this.dataService.triggerReport(); } } diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts index 74c39613502..8beaae7f10a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts @@ -5,6 +5,7 @@ import { BehaviorSubject, of } from "rxjs"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; +import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service"; import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -29,6 +30,7 @@ describe("IntegrationCardComponent", () => { const mockI18nService = mock(); const activatedRoute = mock(); const mockIntegrationService = mock(); + const mockDatadogIntegrationService = mock(); const dialogService = mock(); const toastService = mock(); @@ -53,6 +55,7 @@ describe("IntegrationCardComponent", () => { { provide: I18nService, useValue: mockI18nService }, { provide: ActivatedRoute, useValue: activatedRoute }, { provide: HecOrganizationIntegrationService, useValue: mockIntegrationService }, + { provide: DatadogOrganizationIntegrationService, useValue: mockDatadogIntegrationService }, { provide: ToastService, useValue: toastService }, { provide: DialogService, useValue: dialogService }, ], diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts index 091de63d7a1..3a243f8eb91 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts @@ -13,17 +13,22 @@ import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rx import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; +import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type"; +import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service"; import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { HecConnectDialogResult, + DatadogConnectDialogResult, HecConnectDialogResultStatus, + DatadogConnectDialogResultStatus, + openDatadogConnectDialog, openHecConnectDialog, } from "../integration-dialog/index"; @@ -64,6 +69,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private dialogService: DialogService, private activatedRoute: ActivatedRoute, private hecOrganizationIntegrationService: HecOrganizationIntegrationService, + private datadogOrganizationIntegrationService: DatadogOrganizationIntegrationService, private toastService: ToastService, private i18nService: I18nService, ) { @@ -131,42 +137,87 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { } async setupConnection() { - // invoke the dialog to connect the integration - const dialog = openHecConnectDialog(this.dialogService, { - data: { - settings: this.integrationSettings, - }, - }); + let dialog: DialogRef; - const result = await lastValueFrom(dialog.closed); - - // the dialog was cancelled - if (!result || !result.success) { + if (this.integrationSettings?.integrationType === null) { return; } - try { - if (result.success === HecConnectDialogResultStatus.Delete) { - await this.deleteHec(); - } - } catch { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("failedToDeleteIntegration"), + if (this.integrationSettings?.integrationType === OrganizationIntegrationType.Datadog) { + dialog = openDatadogConnectDialog(this.dialogService, { + data: { + settings: this.integrationSettings, + }, }); - } - try { - if (result.success === HecConnectDialogResultStatus.Edited) { - await this.saveHec(result); + const result = await lastValueFrom(dialog.closed); + + // the dialog was cancelled + if (!result || !result.success) { + return; } - } catch { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("failedToSaveIntegration"), + + try { + if (result.success === HecConnectDialogResultStatus.Delete) { + await this.deleteDatadog(); + } + } catch { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("failedToDeleteIntegration"), + }); + } + + try { + if (result.success === DatadogConnectDialogResultStatus.Edited) { + await this.saveDatadog(result as DatadogConnectDialogResult); + } + } catch { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("failedToSaveIntegration"), + }); + } + } else { + // invoke the dialog to connect the integration + dialog = openHecConnectDialog(this.dialogService, { + data: { + settings: this.integrationSettings, + }, }); + + const result = await lastValueFrom(dialog.closed); + + // the dialog was cancelled + if (!result || !result.success) { + return; + } + + try { + if (result.success === HecConnectDialogResultStatus.Delete) { + await this.deleteHec(); + } + } catch { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("failedToDeleteIntegration"), + }); + } + + try { + if (result.success === HecConnectDialogResultStatus.Edited) { + await this.saveHec(result as HecConnectDialogResult); + } + } catch { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("failedToSaveIntegration"), + }); + } } } @@ -242,6 +293,69 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { }); } + async saveDatadog(result: DatadogConnectDialogResult) { + if (this.isUpdateAvailable) { + // retrieve org integration and configuration ids + const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; + const orgIntegrationConfigurationId = + this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; + + if (!orgIntegrationId || !orgIntegrationConfigurationId) { + throw Error("Organization Integration ID or Configuration ID is missing"); + } + + // update existing integration and configuration + await this.datadogOrganizationIntegrationService.updateDatadog( + this.organizationId, + orgIntegrationId, + orgIntegrationConfigurationId, + this.integrationSettings.name as OrganizationIntegrationServiceType, + result.url, + result.apiKey, + ); + } else { + // create new integration and configuration + await this.datadogOrganizationIntegrationService.saveDatadog( + this.organizationId, + this.integrationSettings.name as OrganizationIntegrationServiceType, + result.url, + result.apiKey, + ); + } + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("success"), + }); + } + + async deleteDatadog() { + const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; + const orgIntegrationConfigurationId = + this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; + + if (!orgIntegrationId || !orgIntegrationConfigurationId) { + throw Error("Organization Integration ID or Configuration ID is missing"); + } + + const response = await this.datadogOrganizationIntegrationService.deleteDatadog( + this.organizationId, + orgIntegrationId, + orgIntegrationConfigurationId, + ); + + if (response.mustBeOwner) { + this.showMustBeOwnerToast(); + return; + } + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("success"), + }); + } + private showMustBeOwnerToast() { this.toastService.showToast({ variant: "error", diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html new file mode 100644 index 00000000000..c129216b694 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html @@ -0,0 +1,58 @@ +
+ + + {{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }} + +
+ @if (loading) { + + + + } + @if (!loading) { + + + {{ "url" | i18n }} + + + + + {{ "apiKey" | i18n }} + + {{ "apiKey" | i18n }} + + + } +
+ + + + + @if (canDelete) { +
+ +
+ } +
+
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.spec.ts new file mode 100644 index 00000000000..7298087e7e4 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.spec.ts @@ -0,0 +1,171 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { mock } from "jest-mock-extended"; + +import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; +import { IntegrationType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { + ConnectDatadogDialogComponent, + DatadogConnectDialogParams, + DatadogConnectDialogResult, + DatadogConnectDialogResultStatus, + openDatadogConnectDialog, +} from "./connect-dialog-datadog.component"; + +beforeAll(() => { + // Mock element.animate for jsdom + // the animate function is not available in jsdom, so we provide a mock implementation + // This is necessary for tests that rely on animations + // This mock does not perform any actual animations, it just provides a structure that allows tests + // to run without throwing errors related to missing animate function + if (!HTMLElement.prototype.animate) { + HTMLElement.prototype.animate = function () { + return { + play: () => {}, + pause: () => {}, + finish: () => {}, + cancel: () => {}, + reverse: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + onfinish: null, + oncancel: null, + startTime: 0, + currentTime: 0, + playbackRate: 1, + playState: "idle", + replaceState: "active", + effect: null, + finished: Promise.resolve(), + id: "", + remove: () => {}, + timeline: null, + ready: Promise.resolve(), + } as unknown as Animation; + }; + } +}); + +describe("ConnectDialogDatadogComponent", () => { + let component: ConnectDatadogDialogComponent; + let fixture: ComponentFixture; + let dialogRefMock = mock>(); + const mockI18nService = mock(); + + const integrationMock: Integration = { + name: "Test Integration", + image: "test-image.png", + linkURL: "https://example.com", + imageDarkMode: "test-image-dark.png", + newBadgeExpiration: "2024-12-31", + description: "Test Description", + canSetupConnection: true, + type: IntegrationType.EVENT, + } as Integration; + const connectInfo: DatadogConnectDialogParams = { + settings: integrationMock, // Provide appropriate mock template if needed + }; + + beforeEach(async () => { + dialogRefMock = mock>(); + + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, SharedModule, BrowserAnimationsModule], + providers: [ + FormBuilder, + { provide: DIALOG_DATA, useValue: connectInfo }, + { provide: DialogRef, useValue: dialogRefMock }, + { provide: I18nPipe, useValue: mock() }, + { provide: I18nService, useValue: mockI18nService }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConnectDatadogDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + mockI18nService.t.mockImplementation((key) => key); + }); + + it("should create the component", () => { + expect(component).toBeTruthy(); + }); + + it("should initialize form with empty values", () => { + expect(component.formGroup.value).toEqual({ + url: "", + apiKey: "", + service: "Test Integration", + }); + }); + + it("should have required validators for all fields", () => { + component.formGroup.setValue({ url: "", apiKey: "", service: "" }); + expect(component.formGroup.valid).toBeFalsy(); + + component.formGroup.setValue({ + url: "https://test.com", + apiKey: "token", + service: "Test Service", + }); + expect(component.formGroup.valid).toBeTruthy(); + }); + + it("should test url is at least 7 characters long", () => { + component.formGroup.setValue({ + url: "test", + apiKey: "token", + service: "Test Service", + }); + expect(component.formGroup.valid).toBeFalsy(); + + component.formGroup.setValue({ + url: "https://test.com", + apiKey: "token", + service: "Test Service", + }); + expect(component.formGroup.valid).toBeTruthy(); + }); + + it("should call dialogRef.close with correct result on submit", async () => { + component.formGroup.setValue({ + url: "https://test.com", + apiKey: "token", + service: "Test Service", + }); + + await component.submit(); + + expect(dialogRefMock.close).toHaveBeenCalledWith({ + integrationSettings: integrationMock, + url: "https://test.com", + apiKey: "token", + service: "Test Service", + success: DatadogConnectDialogResultStatus.Edited, + }); + }); +}); + +describe("openDatadogConnectDialog", () => { + it("should call dialogService.open with correct params", () => { + const dialogServiceMock = mock(); + const config: DialogConfig< + DatadogConnectDialogParams, + DialogRef + > = { + data: { settings: { name: "Test" } as Integration }, + } as any; + + openDatadogConnectDialog(dialogServiceMock, config); + + expect(dialogServiceMock.open).toHaveBeenCalledWith(ConnectDatadogDialogComponent, config); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts new file mode 100644 index 00000000000..d186910d2f7 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts @@ -0,0 +1,121 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; + +import { DatadogConfiguration } from "@bitwarden/bit-common/dirt/organization-integrations/models/configuration/datadog-configuration"; +import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; +import { HecTemplate } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-configuration-config/configuration-template/hec-template"; +import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +export type DatadogConnectDialogParams = { + settings: Integration; +}; + +export interface DatadogConnectDialogResult { + integrationSettings: Integration; + url: string; + apiKey: string; + service: string; + success: DatadogConnectDialogResultStatusType | null; +} + +export const DatadogConnectDialogResultStatus = { + Edited: "edit", + Delete: "delete", +} as const; + +export type DatadogConnectDialogResultStatusType = + (typeof DatadogConnectDialogResultStatus)[keyof typeof DatadogConnectDialogResultStatus]; + +@Component({ + templateUrl: "./connect-dialog-datadog.component.html", + imports: [SharedModule], +}) +export class ConnectDatadogDialogComponent implements OnInit { + loading = false; + datadogConfig: DatadogConfiguration | null = null; + hecTemplate: HecTemplate | null = null; + formGroup = this.formBuilder.group({ + url: ["", [Validators.required, Validators.minLength(7)]], + apiKey: ["", Validators.required], + service: ["", Validators.required], + }); + + constructor( + @Inject(DIALOG_DATA) protected connectInfo: DatadogConnectDialogParams, + protected formBuilder: FormBuilder, + private dialogRef: DialogRef, + private dialogService: DialogService, + ) {} + + ngOnInit(): void { + this.datadogConfig = + this.connectInfo.settings.organizationIntegration?.getConfiguration() ?? + null; + this.hecTemplate = + this.connectInfo.settings.organizationIntegration?.integrationConfiguration?.[0]?.getTemplate() ?? + null; + + this.formGroup.patchValue({ + url: this.datadogConfig?.uri || "", + apiKey: this.datadogConfig?.apiKey || "", + service: this.connectInfo.settings.name, + }); + } + + get isUpdateAvailable(): boolean { + return !!this.datadogConfig; + } + + get canDelete(): boolean { + return !!this.datadogConfig; + } + + submit = async (): Promise => { + if (this.formGroup.invalid) { + this.formGroup.markAllAsTouched(); + return; + } + const result = this.getDatadogConnectDialogResult(DatadogConnectDialogResultStatus.Edited); + + this.dialogRef.close(result); + + return; + }; + + delete = async (): Promise => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: "deleteItemConfirmation", + }, + type: "warning", + }); + + if (confirmed) { + const result = this.getDatadogConnectDialogResult(DatadogConnectDialogResultStatus.Delete); + this.dialogRef.close(result); + } + }; + + private getDatadogConnectDialogResult( + status: DatadogConnectDialogResultStatusType, + ): DatadogConnectDialogResult { + const formJson = this.formGroup.getRawValue(); + + return { + integrationSettings: this.connectInfo.settings, + url: formJson.url || "", + apiKey: formJson.apiKey || "", + service: formJson.service || "", + success: status, + }; + } +} + +export function openDatadogConnectDialog( + dialogService: DialogService, + config: DialogConfig>, +) { + return dialogService.open(ConnectDatadogDialogComponent, config); +} diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/index.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/index.ts index 8c4891b9aa8..9852f3fe5c8 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/index.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/index.ts @@ -1 +1,2 @@ export * from "./connect-dialog/connect-dialog-hec.component"; +export * from "./connect-dialog/connect-dialog-datadog.component"; diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.spec.ts index f0b371703f6..2908fe0c089 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.spec.ts @@ -6,6 +6,7 @@ import { of } from "rxjs"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; +import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service"; import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service"; import { IntegrationType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -24,6 +25,7 @@ describe("IntegrationGridComponent", () => { let fixture: ComponentFixture; const mockActivatedRoute = mock(); const mockIntegrationService = mock(); + const mockDatadogIntegrationService = mock(); const integrations: Integration[] = [ { name: "Integration 1", @@ -70,6 +72,7 @@ describe("IntegrationGridComponent", () => { useValue: mockActivatedRoute, }, { provide: HecOrganizationIntegrationService, useValue: mockIntegrationService }, + { provide: DatadogOrganizationIntegrationService, useValue: mockDatadogIntegrationService }, { provide: ToastService, useValue: mock(), diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts index c249bf42282..539da9b31b1 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts @@ -4,6 +4,8 @@ import { firstValueFrom, Observable, Subject, switchMap, takeUntil, takeWhile } import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; import { OrganizationIntegrationServiceType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; +import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type"; +import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service"; import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -226,6 +228,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { // Sets the organization ID which also loads the integrations$ this.organization$.pipe(takeUntil(this.destroy$)).subscribe((org) => { this.hecOrganizationIntegrationService.setOrganizationIntegrations(org.id); + this.datadogOrganizationIntegrationService.setOrganizationIntegrations(org.id); }); // For all existing event based configurations loop through and assign the @@ -253,6 +256,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { private accountService: AccountService, private configService: ConfigService, private hecOrganizationIntegrationService: HecOrganizationIntegrationService, + private datadogOrganizationIntegrationService: DatadogOrganizationIntegrationService, ) { this.configService .getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations) @@ -270,10 +274,62 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { type: IntegrationType.EVENT, description: "crowdstrikeEventIntegrationDesc", canSetupConnection: true, + integrationType: 5, // Assuming 5 corresponds to CrowdStrike in OrganizationIntegrationType }; this.integrationsList.push(crowdstrikeIntegration); + + const datadogIntegration: Integration = { + name: OrganizationIntegrationServiceType.Datadog, + // TODO: Update link when help article is published + linkURL: "", + image: "../../../../../../../images/integrations/logo-datadog-color.svg", + type: IntegrationType.EVENT, + description: "datadogEventIntegrationDesc", + canSetupConnection: true, + integrationType: 6, // Assuming 6 corresponds to Datadog in OrganizationIntegrationType + }; + + this.integrationsList.push(datadogIntegration); } + + // For all existing event based configurations loop through and assign the + // organizationIntegration for the correct services. + this.hecOrganizationIntegrationService.integrations$ + .pipe(takeUntil(this.destroy$)) + .subscribe((integrations) => { + // reset all integrations to null first - in case one was deleted + this.integrationsList.forEach((i) => { + if (i.integrationType === OrganizationIntegrationType.Hec) { + i.organizationIntegration = null; + } + }); + + integrations.map((integration) => { + const item = this.integrationsList.find((i) => i.name === integration.serviceType); + if (item) { + item.organizationIntegration = integration; + } + }); + }); + + this.datadogOrganizationIntegrationService.integrations$ + .pipe(takeUntil(this.destroy$)) + .subscribe((integrations) => { + // reset all integrations to null first - in case one was deleted + this.integrationsList.forEach((i) => { + if (i.integrationType === OrganizationIntegrationType.Datadog) { + i.organizationIntegration = null; + } + }); + + integrations.map((integration) => { + const item = this.integrationsList.find((i) => i.name === integration.serviceType); + if (item) { + item.organizationIntegration = integration; + } + }); + }); } ngOnDestroy(): void { this.destroy$.next(); diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts index a8e0899f26d..e3c37b4a42b 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; +import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service"; import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service"; import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service"; import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service"; @@ -12,6 +13,11 @@ import { OrganizationIntegrationsRoutingModule } from "./organization-integratio @NgModule({ imports: [AdminConsoleIntegrationsComponent, OrganizationIntegrationsRoutingModule], providers: [ + safeProvider({ + provide: DatadogOrganizationIntegrationService, + useClass: DatadogOrganizationIntegrationService, + deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService], + }), safeProvider({ provide: HecOrganizationIntegrationService, useClass: HecOrganizationIntegrationService, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts index bd105fc21e2..43d512439f0 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -9,6 +9,7 @@ import {} from "@bitwarden/web-vault/app/shared"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { DatadogOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/datadog-organization-integration-service"; import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; @@ -37,6 +38,7 @@ class MockNewMenuComponent {} describe("IntegrationsComponent", () => { let fixture: ComponentFixture; const hecOrgIntegrationSvc = mock(); + const datadogOrgIntegrationSvc = mock(); const activatedRouteMock = { snapshot: { paramMap: { get: jest.fn() } }, @@ -55,6 +57,7 @@ describe("IntegrationsComponent", () => { { provide: I18nPipe, useValue: mock() }, { provide: I18nService, useValue: mockI18nService }, { provide: HecOrganizationIntegrationService, useValue: hecOrgIntegrationSvc }, + { provide: DatadogOrganizationIntegrationService, useValue: datadogOrgIntegrationSvc }, ], }).compileComponents(); fixture = TestBed.createComponent(IntegrationsComponent); 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 d9ff3ec5619..e301c0462c3 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 @@ -23,8 +23,6 @@ import { 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 { 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -117,7 +115,6 @@ export class OverviewComponent implements OnInit, OnDestroy { private smOnboardingTasksService: SMOnboardingTasksService, private logService: LogService, private router: Router, - private configService: ConfigService, ) {} ngOnInit() { @@ -218,13 +215,12 @@ export class OverviewComponent implements OnInit, OnDestroy { } async navigateToPaymentMethod() { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, + await this.router.navigate( + ["organizations", `${this.organizationId}`, "billing", "payment-details"], + { + state: { launchPaymentModalAutomatically: true }, + }, ); - const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; - await this.router.navigate(["organizations", `${this.organizationId}`, "billing", route], { - state: { launchPaymentModalAutomatically: true }, - }); } ngOnDestroy(): void { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts index 13f80920558..ec7397a22a8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts @@ -41,7 +41,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { return convertToAccessPolicyItemViews(policies); }), ), - catchError(async () => { + catchError(async (): Promise => { this.logService.info("Error fetching project people access policies."); await this.router.navigate(["/sm", this.organizationId, "projects"]); return undefined; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html index 3d7fc9715c3..f2fb49b73f4 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html @@ -84,6 +84,15 @@ {{ "editMachineAccount" | i18n }} + + + + +

{{ "passkeyAuthenticationFailed" | i18n }}

+ +
+ +

+ {{ "troubleLoggingIn" | i18n }}
+ {{ "useADifferentLogInMethod" | i18n }} +

+
diff --git a/libs/angular/src/auth/components/base-login-via-webauthn.component.ts b/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.ts similarity index 62% rename from libs/angular/src/auth/components/base-login-via-webauthn.component.ts rename to libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.ts index 53e29d4d940..f795b66d916 100644 --- a/libs/angular/src/auth/components/base-login-via-webauthn.component.ts +++ b/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.ts @@ -1,27 +1,69 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { Router, RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + TwoFactorAuthSecurityKeyIcon, + TwoFactorAuthSecurityKeyFailedIcon, +} from "@bitwarden/assets/svg"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { LoginSuccessHandlerService } from "@bitwarden/auth/common"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view"; +import { ClientType } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; 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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { + AnonLayoutWrapperDataService, + ButtonModule, + IconModule, + LinkModule, + TypographyModule, +} from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; export type State = "assert" | "assertFailed"; - -@Directive() -export class BaseLoginViaWebAuthnComponent implements OnInit { +@Component({ + selector: "app-login-via-webauthn", + templateUrl: "login-via-webauthn.component.html", + standalone: true, + imports: [ + CommonModule, + RouterModule, + JslibModule, + ButtonModule, + IconModule, + LinkModule, + TypographyModule, + ], +}) +export class LoginViaWebAuthnComponent implements OnInit { protected currentState: State = "assert"; - protected successRoute = "/vault"; + protected readonly Icons = { + TwoFactorAuthSecurityKeyIcon, + TwoFactorAuthSecurityKeyFailedIcon, + }; + + private readonly successRoutes: Record = { + [ClientType.Web]: "/vault", + [ClientType.Browser]: "/tabs/vault", + [ClientType.Desktop]: "/vault", + [ClientType.Cli]: "/vault", + }; + + protected get successRoute(): string { + const clientType = this.platformUtilsService.getClientType(); + return this.successRoutes[clientType] || "/vault"; + } constructor( private webAuthnLoginService: WebAuthnLoginServiceAbstraction, @@ -31,6 +73,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { private i18nService: I18nService, private loginSuccessHandlerService: LoginSuccessHandlerService, private keyService: KeyService, + private platformUtilsService: PlatformUtilsService, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, ) {} ngOnInit(): void { @@ -41,6 +85,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { protected retry() { this.currentState = "assert"; + // Reset to default icon on retry + this.setDefaultIcon(); // 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.authenticate(); @@ -54,6 +100,7 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { } catch (error) { this.validationService.showError(error); this.currentState = "assertFailed"; + this.setFailureIcon(); return; } try { @@ -64,6 +111,7 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"), ); this.currentState = "assertFailed"; + this.setFailureIcon(); return; } @@ -80,6 +128,19 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { } this.logService.error(error); this.currentState = "assertFailed"; + this.setFailureIcon(); } } + + private setDefaultIcon(): void { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageIcon: this.Icons.TwoFactorAuthSecurityKeyIcon, + }); + } + + private setFailureIcon(): void { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageIcon: this.Icons.TwoFactorAuthSecurityKeyFailedIcon, + }); + } } diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html deleted file mode 100644 index c9c0c296ada..00000000000 --- a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html +++ /dev/null @@ -1,55 +0,0 @@ -
- - -

{{ "creditDelayed" | i18n }}

-
- - - {{ "payPal" | i18n }} - - - {{ "bitcoin" | i18n }} - - -
-
- - {{ "amount" | i18n }} - - $USD - -
-
- - - - -
-
-
- - - - - - - - - - - - - - - -
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 deleted file mode 100644 index 3dc56c55b0c..00000000000 --- a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts +++ /dev/null @@ -1,167 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core"; -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 { 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, 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"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { UserId } from "@bitwarden/common/types/guid"; -import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; - -export type AddAccountCreditDialogParams = { - organizationId?: string; - providerId?: string; -}; - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum AddAccountCreditDialogResultType { - Closed = "closed", - Submitted = "submitted", -} - -export const openAddAccountCreditDialog = ( - dialogService: DialogService, - dialogConfig: DialogConfig, -) => - dialogService.open( - AddAccountCreditDialogComponent, - dialogConfig, - ); - -type PayPalConfig = { - businessId?: string; - buttonAction?: string; - returnUrl?: string; - customField?: string; - subject?: string; -}; - -@Component({ - templateUrl: "./add-account-credit-dialog.component.html", - standalone: false, -}) -export class AddAccountCreditDialogComponent implements OnInit { - @ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm: ElementRef; - protected formGroup = new FormGroup({ - paymentMethod: new FormControl(PaymentMethodType.PayPal), - creditAmount: new FormControl(null, [Validators.required, Validators.min(0.01)]), - }); - protected payPalConfig: PayPalConfig; - protected ResultType = AddAccountCreditDialogResultType; - - private organization?: Organization; - private provider?: Provider; - private user?: { id: UserId } & AccountInfo; - - constructor( - private accountService: AccountService, - private apiService: ApiService, - private configService: ConfigService, - @Inject(DIALOG_DATA) private dialogParams: AddAccountCreditDialogParams, - private dialogRef: DialogRef, - private organizationService: OrganizationService, - private platformUtilsService: PlatformUtilsService, - private providerService: ProviderService, - ) { - this.payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig; - } - - protected readonly paymentMethodType = PaymentMethodType; - - submit = async () => { - this.formGroup.markAllAsTouched(); - - if (this.formGroup.invalid) { - return; - } - - if (this.formGroup.value.paymentMethod === PaymentMethodType.PayPal) { - this.payPalForm.nativeElement.submit(); - return; - } - - if (this.formGroup.value.paymentMethod === PaymentMethodType.BitPay) { - const request = this.getBitPayInvoiceRequest(); - const bitPayUrl = await this.apiService.postBitPayInvoice(request); - this.platformUtilsService.launchUri(bitPayUrl); - return; - } - - this.dialogRef.close(AddAccountCreditDialogResultType.Submitted); - }; - - async ngOnInit(): Promise { - let payPalCustomField: string; - - if (this.dialogParams.organizationId) { - this.formGroup.patchValue({ - creditAmount: 20.0, - }); - 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) { - this.formGroup.patchValue({ - creditAmount: 20.0, - }); - this.provider = await firstValueFrom( - this.providerService.get$(this.dialogParams.providerId, this.user.id), - ); - payPalCustomField = "provider_id:" + this.provider.id; - this.payPalConfig.subject = this.provider.name; - } else { - this.formGroup.patchValue({ - creditAmount: 10.0, - }); - payPalCustomField = "user_id:" + this.user.id; - this.payPalConfig.subject = this.user.email; - } - - const region = await firstValueFrom(this.configService.cloudRegion$); - - payPalCustomField += ",account_credit:1"; - payPalCustomField += `,region:${region}`; - - this.payPalConfig.customField = payPalCustomField; - this.payPalConfig.returnUrl = window.location.href; - } - - getBitPayInvoiceRequest(): BitPayInvoiceRequest { - const request = new BitPayInvoiceRequest(); - if (this.organization) { - request.name = this.organization.name; - request.organizationId = this.organization.id; - } else if (this.provider) { - request.name = this.provider.name; - request.providerId = this.provider.id; - } else { - request.email = this.user.email; - request.userId = this.user.id; - } - - request.credit = true; - request.amount = this.formGroup.value.creditAmount; - request.returnUrl = window.location.href; - - return request; - } -} diff --git a/libs/angular/src/billing/components/index.ts b/libs/angular/src/billing/components/index.ts index dacb5b265bd..34e1d27c1ed 100644 --- a/libs/angular/src/billing/components/index.ts +++ b/libs/angular/src/billing/components/index.ts @@ -1,5 +1 @@ -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/billing/components/manage-tax-information/manage-tax-information.component.html b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html deleted file mode 100644 index 391765251b0..00000000000 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html +++ /dev/null @@ -1,90 +0,0 @@ -
-
-
- - {{ "country" | i18n }} - - - - -
-
- - {{ "zipPostalCode" | i18n }} - - -
- -
- - {{ "address1" | i18n }} - - -
-
- - {{ "address2" | i18n }} - - -
-
- - {{ "cityTown" | i18n }} - - -
-
- - {{ "stateProvince" | i18n }} - - -
-
- - {{ "taxIdNumber" | i18n }} - - -
-
-
- -
-
-
diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.spec.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.spec.ts deleted file mode 100644 index c662e20b275..00000000000 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.spec.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { SimpleChange } from "@angular/core"; -import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { ReactiveFormsModule } from "@angular/forms"; -import { mock, MockProxy } from "jest-mock-extended"; - -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { SelectModule, FormFieldModule, BitSubmitDirective } from "@bitwarden/components"; -import { I18nPipe } from "@bitwarden/ui-common"; - -import { ManageTaxInformationComponent } from "./manage-tax-information.component"; - -describe("ManageTaxInformationComponent", () => { - let sut: ManageTaxInformationComponent; - let fixture: ComponentFixture; - let mockTaxService: MockProxy; - - beforeEach(async () => { - mockTaxService = mock(); - await TestBed.configureTestingModule({ - declarations: [ManageTaxInformationComponent], - providers: [ - { provide: TaxServiceAbstraction, useValue: mockTaxService }, - { provide: I18nService, useValue: { t: (key: string) => key } }, - ], - imports: [ - CommonModule, - ReactiveFormsModule, - SelectModule, - FormFieldModule, - BitSubmitDirective, - I18nPipe, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(ManageTaxInformationComponent); - sut = fixture.componentInstance; - fixture.autoDetectChanges(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it("creates successfully", () => { - expect(sut).toBeTruthy(); - }); - - it("should initialize with all values empty in startWith", async () => { - // Arrange - sut.startWith = { - country: "", - postalCode: "", - taxId: "", - line1: "", - line2: "", - city: "", - state: "", - }; - - // Act - fixture.detectChanges(); - - // Assert - const startWithValue = sut.startWith; - expect(startWithValue.line1).toHaveLength(0); - expect(startWithValue.line2).toHaveLength(0); - expect(startWithValue.city).toHaveLength(0); - expect(startWithValue.state).toHaveLength(0); - expect(startWithValue.postalCode).toHaveLength(0); - expect(startWithValue.country).toHaveLength(0); - expect(startWithValue.taxId).toHaveLength(0); - }); - - it("should update the tax information protected state when form is updated", async () => { - // Arrange - const line1Value = "123 Street"; - const line2Value = "Apt. 5"; - const cityValue = "New York"; - const stateValue = "NY"; - const countryValue = "USA"; - const postalCodeValue = "123 Street"; - - sut.startWith = { - country: countryValue, - postalCode: "", - taxId: "", - line1: "", - line2: "", - city: "", - state: "", - }; - sut.showTaxIdField = false; - mockTaxService.isCountrySupported.mockResolvedValue(true); - - // Act - await sut.ngOnInit(); - fixture.detectChanges(); - - const line1: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='line1']", - ); - const line2: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='line2']", - ); - const city: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='city']", - ); - const state: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='state']", - ); - const postalCode: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='postalCode']", - ); - - line1.value = line1Value; - line2.value = line2Value; - city.value = cityValue; - state.value = stateValue; - postalCode.value = postalCodeValue; - - line1.dispatchEvent(new Event("input")); - line2.dispatchEvent(new Event("input")); - city.dispatchEvent(new Event("input")); - state.dispatchEvent(new Event("input")); - postalCode.dispatchEvent(new Event("input")); - await fixture.whenStable(); - - // Assert - - // Assert that the internal tax information reflects the form - const taxInformation = sut.getTaxInformation(); - expect(taxInformation.line1).toBe(line1Value); - expect(taxInformation.line2).toBe(line2Value); - expect(taxInformation.city).toBe(cityValue); - expect(taxInformation.state).toBe(stateValue); - expect(taxInformation.postalCode).toBe(postalCodeValue); - expect(taxInformation.country).toBe(countryValue); - expect(taxInformation.taxId).toHaveLength(0); - - expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); - expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(2); - }); - - it("should not show address fields except postal code if country is not supported for taxes", async () => { - // Arrange - const countryValue = "UNKNOWN"; - sut.startWith = { - country: countryValue, - postalCode: "", - taxId: "", - line1: "", - line2: "", - city: "", - state: "", - }; - sut.showTaxIdField = false; - mockTaxService.isCountrySupported.mockResolvedValue(false); - - // Act - await sut.ngOnInit(); - fixture.detectChanges(); - - const line1: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='line1']", - ); - const line2: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='line2']", - ); - const city: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='city']", - ); - const state: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='state']", - ); - const postalCode: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='postalCode']", - ); - - // Assert - expect(line1).toBeNull(); - expect(line2).toBeNull(); - expect(city).toBeNull(); - expect(state).toBeNull(); - //Should be visible - expect(postalCode).toBeTruthy(); - - expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); - expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1); - }); - - it("should not show the tax id field if showTaxIdField is set to false", async () => { - // Arrange - const countryValue = "USA"; - sut.startWith = { - country: countryValue, - postalCode: "", - taxId: "", - line1: "", - line2: "", - city: "", - state: "", - }; - - sut.showTaxIdField = false; - mockTaxService.isCountrySupported.mockResolvedValue(true); - - // Act - await sut.ngOnInit(); - fixture.detectChanges(); - - // Assert - const taxId: HTMLInputElement = fixture.nativeElement.querySelector( - "input[formControlName='taxId']", - ); - expect(taxId).toBeNull(); - - expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); - expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1); - }); - - it("should clear the tax id field if showTaxIdField is set to false after being true", async () => { - // Arrange - const countryValue = "USA"; - const taxIdValue = "A12345678"; - - sut.startWith = { - country: countryValue, - postalCode: "", - taxId: taxIdValue, - line1: "", - line2: "", - city: "", - state: "", - }; - sut.showTaxIdField = true; - - mockTaxService.isCountrySupported.mockResolvedValue(true); - await sut.ngOnInit(); - fixture.detectChanges(); - const initialTaxIdValue = fixture.nativeElement.querySelector( - "input[formControlName='taxId']", - ).value; - - // Act - sut.showTaxIdField = false; - sut.ngOnChanges({ showTaxIdField: new SimpleChange(true, false, false) }); - fixture.detectChanges(); - - // Assert - const taxId = fixture.nativeElement.querySelector("input[formControlName='taxId']"); - expect(taxId).toBeNull(); - - const taxInformation = sut.getTaxInformation(); - expect(taxInformation.taxId).toBeNull(); - expect(initialTaxIdValue).toEqual(taxIdValue); - - expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue); - expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1); - }); -}); diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts deleted file mode 100644 index 0b87f3f931d..00000000000 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts +++ /dev/null @@ -1,166 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { - Component, - EventEmitter, - Input, - OnChanges, - OnDestroy, - OnInit, - Output, - SimpleChanges, -} from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; -import { debounceTime } from "rxjs/operators"; - -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/models/domain"; - -@Component({ - selector: "app-manage-tax-information", - templateUrl: "./manage-tax-information.component.html", - standalone: false, -}) -export class ManageTaxInformationComponent implements OnInit, OnDestroy, OnChanges { - @Input() startWith: TaxInformation; - @Input() onSubmit?: (taxInformation: TaxInformation) => Promise; - @Input() showTaxIdField: boolean = true; - - /** - * Emits when the tax information has changed. - */ - @Output() taxInformationChanged = new EventEmitter(); - - /** - * Emits when the tax information has been updated. - */ - @Output() taxInformationUpdated = new EventEmitter(); - - private taxInformation: TaxInformation; - - protected formGroup = this.formBuilder.group({ - country: ["", Validators.required], - postalCode: ["", Validators.required], - taxId: "", - line1: "", - line2: "", - city: "", - state: "", - }); - - protected isTaxSupported: boolean; - - private destroy$ = new Subject(); - - protected readonly countries: CountryListItem[] = this.taxService.getCountries(); - - constructor( - private formBuilder: FormBuilder, - private taxService: TaxServiceAbstraction, - ) {} - - getTaxInformation(): TaxInformation { - return this.taxInformation; - } - - submit = async () => { - this.markAllAsTouched(); - if (this.formGroup.invalid) { - return; - } - await this.onSubmit?.(this.taxInformation); - this.taxInformationUpdated.emit(); - }; - - validate(): boolean { - this.markAllAsTouched(); - return this.formGroup.valid; - } - - markAllAsTouched() { - this.formGroup.markAllAsTouched(); - } - - async ngOnInit() { - this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((values) => { - this.taxInformation = { - country: values.country, - postalCode: values.postalCode, - taxId: values.taxId, - line1: values.line1, - line2: values.line2, - city: values.city, - state: values.state, - }; - }); - - if (this.startWith) { - this.formGroup.controls.country.setValue(this.startWith.country); - this.formGroup.controls.postalCode.setValue(this.startWith.postalCode); - - this.isTaxSupported = - this.startWith && this.startWith.country - ? await this.taxService.isCountrySupported(this.startWith.country) - : false; - - if (this.isTaxSupported) { - this.formGroup.controls.taxId.setValue(this.startWith.taxId); - this.formGroup.controls.line1.setValue(this.startWith.line1); - this.formGroup.controls.line2.setValue(this.startWith.line2); - this.formGroup.controls.city.setValue(this.startWith.city); - this.formGroup.controls.state.setValue(this.startWith.state); - } - } - - this.formGroup.controls.country.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe((country: string) => { - this.taxService - .isCountrySupported(country) - .then((isSupported) => (this.isTaxSupported = isSupported)) - .catch(() => (this.isTaxSupported = false)) - .finally(() => { - if (!this.isTaxSupported) { - this.formGroup.controls.taxId.setValue(null); - this.formGroup.controls.line1.setValue(null); - this.formGroup.controls.line2.setValue(null); - this.formGroup.controls.city.setValue(null); - this.formGroup.controls.state.setValue(null); - } - if (this.taxInformationChanged) { - this.taxInformationChanged.emit(this.taxInformation); - } - }); - }); - - this.formGroup.controls.postalCode.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - if (this.taxInformationChanged) { - this.taxInformationChanged.emit(this.taxInformation); - } - }); - - this.formGroup.controls.taxId.valueChanges - .pipe(debounceTime(1000), takeUntil(this.destroy$)) - .subscribe(() => { - if (this.taxInformationChanged) { - this.taxInformationChanged.emit(this.taxInformation); - } - }); - } - - ngOnChanges(changes: SimpleChanges): void { - // Clear the value of the tax-id if states have been changed in the parent component - const showTaxIdField = changes["showTaxIdField"]; - if (showTaxIdField && !showTaxIdField.currentValue) { - this.formGroup.controls.taxId.setValue(null); - } - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } -} diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index c0bf1425d47..446530a1111 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -2,12 +2,6 @@ import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { - AddAccountCreditDialogComponent, - InvoicesComponent, - NoInvoicesComponent, - ManageTaxInformationComponent, -} from "@bitwarden/angular/billing/components"; import { AsyncActionsModule, AutofocusDirective, @@ -112,10 +106,6 @@ import { IconComponent } from "./vault/components/icon.component"; UserTypePipe, IfFeatureDirective, FingerprintPipe, - AddAccountCreditDialogComponent, - InvoicesComponent, - NoInvoicesComponent, - ManageTaxInformationComponent, TwoFactorIconComponent, ], exports: [ @@ -146,10 +136,6 @@ import { IconComponent } from "./vault/components/icon.component"; UserTypePipe, IfFeatureDirective, FingerprintPipe, - AddAccountCreditDialogComponent, - InvoicesComponent, - NoInvoicesComponent, - ManageTaxInformationComponent, TwoFactorIconComponent, TextDragDirective, ], diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index ff704394bc3..df135dcc0ef 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -20,11 +20,13 @@ import { import { DefaultLoginComponentService, DefaultLoginDecryptionOptionsService, + DefaultNewDeviceVerificationComponentService, DefaultRegistrationFinishService, DefaultTwoFactorAuthComponentService, DefaultTwoFactorAuthWebAuthnComponentService, LoginComponentService, LoginDecryptionOptionsService, + NewDeviceVerificationComponentService, RegistrationFinishService as RegistrationFinishServiceAbstraction, TwoFactorAuthComponentService, TwoFactorAuthWebAuthnComponentService, @@ -102,6 +104,7 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@ import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction"; import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; +import { SendTokenService, DefaultSendTokenService } from "@bitwarden/common/auth/send-access"; import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service"; @@ -144,14 +147,12 @@ import { AccountBillingApiServiceAbstraction } from "@bitwarden/common/billing/a import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; import { OrganizationSponsorshipApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { AccountBillingApiService } from "@bitwarden/common/billing/services/account/account-billing-api.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service"; import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service"; import { OrganizationSponsorshipApiService } from "@bitwarden/common/billing/services/organization/organization-sponsorship-api.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; -import { TaxService } from "@bitwarden/common/billing/services/tax.service"; import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service"; import { DefaultKeyGenerationService, @@ -264,6 +265,7 @@ import { InternalSendService, SendService as SendServiceAbstraction, } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; @@ -284,6 +286,7 @@ import { DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service"; import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; @@ -296,7 +299,6 @@ import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks"; import { AnonLayoutWrapperDataService, DefaultAnonLayoutWrapperDataService, - DialogService, ToastService, } from "@bitwarden/components"; import { @@ -345,11 +347,7 @@ import { import { SafeInjectionToken } from "@bitwarden/ui-common"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { - CipherArchiveService, - DefaultCipherArchiveService, - PasswordRepromptService, -} from "@bitwarden/vault"; +import { PasswordRepromptService } from "@bitwarden/vault"; import { IndividualVaultExportService, IndividualVaultExportServiceAbstraction, @@ -1401,11 +1399,6 @@ const safeProviders: SafeProvider[] = [ useClass: BillingApiService, deps: [ApiServiceAbstraction], }), - safeProvider({ - provide: TaxServiceAbstraction, - useClass: TaxService, - deps: [ApiServiceAbstraction], - }), safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, @@ -1596,6 +1589,11 @@ const safeProviders: SafeProvider[] = [ MessageListener, ], }), + safeProvider({ + provide: SendTokenService, + useClass: DefaultSendTokenService, + deps: [GlobalStateProvider, SdkService, SendPasswordService], + }), safeProvider({ provide: EndUserNotificationService, useClass: DefaultEndUserNotificationService, @@ -1652,12 +1650,15 @@ const safeProviders: SafeProvider[] = [ deps: [ CipherServiceAbstraction, ApiServiceAbstraction, - DialogService, - PasswordRepromptService, BillingAccountProfileStateService, ConfigService, ], }), + safeProvider({ + provide: NewDeviceVerificationComponentService, + useClass: DefaultNewDeviceVerificationComponentService, + deps: [], + }), ]; @NgModule({ diff --git a/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts b/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts index feaaf74c96d..e9a6923c2fb 100644 --- a/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts @@ -4,7 +4,7 @@ import { Directive, EventEmitter, Input, Output } from "@angular/core"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionTypes, CollectionView } from "@bitwarden/admin-console/common"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; import { DynamicTreeNode } from "../models/dynamic-tree-node.model"; @@ -21,6 +21,7 @@ export class CollectionFilterComponent { @Output() onNodeCollapseStateChange: EventEmitter = new EventEmitter(); @Output() onFilterChange: EventEmitter = new EventEmitter(); + DefaultCollectionType = CollectionTypes.DefaultUserCollection; readonly collectionsGrouping: TopLevelTreeNode = { id: "collections", diff --git a/libs/assets/src/svg/svgs/favorites.icon.ts b/libs/assets/src/svg/svgs/favorites.icon.ts new file mode 100644 index 00000000000..4725d0b0a7c --- /dev/null +++ b/libs/assets/src/svg/svgs/favorites.icon.ts @@ -0,0 +1,26 @@ +import { svgIcon } from "../icon-service"; + +export const FavoritesIcon = svgIcon` + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/libs/assets/src/svg/svgs/index.ts b/libs/assets/src/svg/svgs/index.ts index ab4f2c23f13..40a91f78d4d 100644 --- a/libs/assets/src/svg/svgs/index.ts +++ b/libs/assets/src/svg/svgs/index.ts @@ -12,6 +12,7 @@ export * from "./deactivated-org"; export * from "./devices.icon"; export * from "./domain.icon"; export * from "./empty-trash"; +export * from "./favorites.icon"; export * from "./gear"; export * from "./generator"; export * from "./item-types"; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index fdd06509511..454a9091c25 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -59,6 +59,8 @@ export * from "./two-factor-auth"; // device verification export * from "./new-device-verification/new-device-verification.component"; +export * from "./new-device-verification/new-device-verification-component.service"; +export * from "./new-device-verification/default-new-device-verification-component.service"; // validators export * from "./validators/compare-inputs.validator"; diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 91a421216f2..9ade2c1d0af 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -34,6 +34,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; 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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -138,6 +139,7 @@ export class LoginComponent implements OnInit, OnDestroy { private loginSuccessHandlerService: LoginSuccessHandlerService, private configService: ConfigService, private ssoLoginService: SsoLoginServiceAbstraction, + private environmentService: EnvironmentService, ) { this.clientType = this.platformUtilsService.getClientType(); } @@ -307,7 +309,7 @@ export class LoginComponent implements OnInit, OnDestroy { await this.handleAuthResult(authResult); } catch (error) { this.logService.error(error); - this.handleSubmitError(error); + await this.handleSubmitError(error); } }; @@ -316,15 +318,18 @@ export class LoginComponent implements OnInit, OnDestroy { * * @param error The error object. */ - private handleSubmitError(error: unknown) { + private async handleSubmitError(error: unknown) { // Handle error responses if (error instanceof ErrorResponse) { switch (error.statusCode) { case HttpStatusCode.BadRequest: { if (error.message?.toLowerCase().includes("username or password is incorrect")) { + const env = await firstValueFrom(this.environmentService.environment$); + const host = Utils.getHost(env.getWebVaultUrl()); + this.formGroup.controls.masterPassword.setErrors({ error: { - message: this.i18nService.t("invalidMasterPassword"), + message: this.i18nService.t("invalidMasterPasswordConfirmEmailAndHost", host), }, }); } else { diff --git a/libs/auth/src/angular/new-device-verification/default-new-device-verification-component.service.spec.ts b/libs/auth/src/angular/new-device-verification/default-new-device-verification-component.service.spec.ts new file mode 100644 index 00000000000..a2ea26268ea --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/default-new-device-verification-component.service.spec.ts @@ -0,0 +1,21 @@ +import { DefaultNewDeviceVerificationComponentService } from "./default-new-device-verification-component.service"; + +describe("DefaultNewDeviceVerificationComponentService", () => { + let sut: DefaultNewDeviceVerificationComponentService; + + beforeEach(() => { + sut = new DefaultNewDeviceVerificationComponentService(); + }); + + it("should instantiate the service", () => { + expect(sut).not.toBeFalsy(); + }); + + describe("showBackButton()", () => { + it("should return true", () => { + const result = sut.showBackButton(); + + expect(result).toBe(true); + }); + }); +}); diff --git a/libs/auth/src/angular/new-device-verification/default-new-device-verification-component.service.ts b/libs/auth/src/angular/new-device-verification/default-new-device-verification-component.service.ts new file mode 100644 index 00000000000..88ea652bc4b --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/default-new-device-verification-component.service.ts @@ -0,0 +1,9 @@ +import { NewDeviceVerificationComponentService } from "./new-device-verification-component.service"; + +export class DefaultNewDeviceVerificationComponentService + implements NewDeviceVerificationComponentService +{ + showBackButton() { + return true; + } +} diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification-component.service.ts b/libs/auth/src/angular/new-device-verification/new-device-verification-component.service.ts new file mode 100644 index 00000000000..c34fc40ef00 --- /dev/null +++ b/libs/auth/src/angular/new-device-verification/new-device-verification-component.service.ts @@ -0,0 +1,8 @@ +export abstract class NewDeviceVerificationComponentService { + /** + * States whether component should show a back button. Can be overridden by client-specific component services. + * - Default = `true` + * - Extension = `false` (because Extension shows a back button in the header instead) + */ + abstract showBackButton: () => boolean; +} 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 index e731f3afcb6..814c48db0fc 100644 --- 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 @@ -22,7 +22,7 @@ {{ "resendCode" | i18n }} -
+
+ + @if (showBackButton) { +
{{ "or" | 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 index 6362b901fc8..2211b3390a7 100644 --- 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 @@ -1,4 +1,4 @@ -import { CommonModule } from "@angular/common"; +import { CommonModule, Location } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; import { Router } from "@angular/router"; @@ -11,7 +11,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -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"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. @@ -26,6 +25,8 @@ import { import { LoginStrategyServiceAbstraction } from "../../common/abstractions/login-strategy.service"; +import { NewDeviceVerificationComponentService } from "./new-device-verification-component.service"; + /** * Component for verifying a new device via a one-time password (OTP). */ @@ -57,6 +58,7 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy { protected disableRequestOTP = false; private destroy$ = new Subject(); protected authenticationSessionTimeoutRoute = "/authentication-timeout"; + protected showBackButton = true; constructor( private router: Router, @@ -66,12 +68,15 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy { private logService: LogService, private i18nService: I18nService, private loginSuccessHandlerService: LoginSuccessHandlerService, - private configService: ConfigService, private accountService: AccountService, private masterPasswordService: MasterPasswordServiceAbstraction, + private newDeviceVerificationComponentService: NewDeviceVerificationComponentService, + private location: Location, ) {} async ngOnInit() { + this.showBackButton = this.newDeviceVerificationComponentService.showBackButton(); + // Redirect to timeout route if session expires this.loginStrategyService.authenticationSessionTimeout$ .pipe(takeUntil(this.destroy$)) @@ -179,4 +184,8 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy { codeControl.markAsTouched(); } }; + + protected goBack() { + this.location.back(); + } } 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 f5ba2d0be23..25ae8a31ef6 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 @@ -75,7 +75,7 @@ describe("WebAuthnLoginStrategy", () => { // We must do this to make the mocked classes available for all the // assertCredential(...) tests. - global.PublicKeyCredential = MockPublicKeyCredential; + global.PublicKeyCredential = MockPublicKeyCredential as any; global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse; }); @@ -397,4 +397,8 @@ export class MockPublicKeyCredential implements PublicKeyCredential { static isUserVerifyingPlatformAuthenticatorAvailable(): Promise { return Promise.resolve(false); } + + toJSON() { + throw new Error("Method not implemented."); + } } diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index d746342d728..aea52d7310d 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -77,14 +77,10 @@ import { } from "../auth/models/response/two-factor-web-authn.response"; import { TwoFactorYubiKeyResponse } from "../auth/models/response/two-factor-yubi-key.response"; import { BitPayInvoiceRequest } from "../billing/models/request/bit-pay-invoice.request"; -import { PaymentRequest } from "../billing/models/request/payment.request"; -import { TaxInfoUpdateRequest } from "../billing/models/request/tax-info-update.request"; import { BillingHistoryResponse } from "../billing/models/response/billing-history.response"; -import { BillingPaymentResponse } from "../billing/models/response/billing-payment.response"; import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; -import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request"; import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request"; import { DeleteRecoverRequest } from "../models/request/delete-recover.request"; @@ -171,10 +167,8 @@ export abstract class ApiService { abstract getProfile(): Promise; abstract getUserSubscription(): Promise; - abstract getTaxInfo(): Promise; abstract putProfile(request: UpdateProfileRequest): Promise; abstract putAvatar(request: UpdateAvatarRequest): Promise; - abstract putTaxInfo(request: TaxInfoUpdateRequest): Promise; abstract postPrelogin(request: PreloginRequest): Promise; abstract postEmailToken(request: EmailTokenRequest): Promise; abstract postEmail(request: EmailRequest): Promise; @@ -185,7 +179,6 @@ export abstract class ApiService { abstract postPremium(data: FormData): Promise; abstract postReinstatePremium(): Promise; abstract postAccountStorage(request: StorageRequest): Promise; - abstract postAccountPayment(request: PaymentRequest): Promise; abstract postAccountLicense(data: FormData): Promise; abstract postAccountKeys(request: KeysRequest): Promise; abstract postAccountVerifyEmail(): Promise; @@ -209,7 +202,6 @@ export abstract class ApiService { abstract getLastAuthRequest(): Promise; abstract getUserBillingHistory(): Promise; - abstract getUserBillingPayment(): Promise; abstract getCipher(id: string): Promise; abstract getFullCipherDetails(id: string): Promise; @@ -469,6 +461,13 @@ export abstract class ApiService { end: string, token: string, ): Promise>; + abstract getEventsServiceAccount( + orgId: string, + id: string, + start: string, + end: string, + token: string, + ): Promise>; abstract getEventsProject( orgId: string, id: string, diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index 10626d6758f..6c91c2ea0cf 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -3,21 +3,17 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; -import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request"; import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request"; import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; -import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; -import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response"; import { ImportDirectoryRequest } from "../../../models/request/import-directory.request"; import { SeatRequest } from "../../../models/request/seat.request"; import { StorageRequest } from "../../../models/request/storage.request"; -import { VerifyBankRequest } from "../../../models/request/verify-bank.request"; import { ListResponse } from "../../../models/response/list.response"; import { OrganizationApiKeyType } from "../../enums"; import { OrganizationCollectionManagementUpdateRequest } from "../../models/request/organization-collection-management-update.request"; @@ -45,7 +41,6 @@ export abstract class OrganizationApiServiceAbstraction { ): Promise; abstract createLicense(data: FormData): Promise; abstract save(id: string, request: OrganizationUpdateRequest): Promise; - abstract updatePayment(id: string, request: PaymentRequest): Promise; abstract upgrade(id: string, request: OrganizationUpgradeRequest): Promise; abstract updatePasswordManagerSeats( id: string, @@ -57,7 +52,6 @@ export abstract class OrganizationApiServiceAbstraction { ): Promise; abstract updateSeats(id: string, request: SeatRequest): Promise; abstract updateStorage(id: string, request: StorageRequest): Promise; - abstract verifyBank(id: string, request: VerifyBankRequest): Promise; abstract reinstate(id: string): Promise; abstract leave(id: string): Promise; abstract delete(id: string, request: SecretVerificationRequest): Promise; @@ -76,8 +70,6 @@ export abstract class OrganizationApiServiceAbstraction { organizationApiKeyType?: OrganizationApiKeyType, ): Promise>; abstract rotateApiKey(id: string, request: OrganizationApiKeyRequest): Promise; - abstract getTaxInfo(id: string): Promise; - abstract updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise; abstract getKeys(id: string): Promise; abstract updateKeys( id: string, diff --git a/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts b/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts index 5c9ea5526a0..001bba11cf4 100644 --- a/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts +++ b/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts @@ -1,7 +1,19 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ExpandedTaxInfoUpdateRequest } from "../../../../billing/models/request/expanded-tax-info-update.request"; -import { TokenizedPaymentSourceRequest } from "../../../../billing/models/request/tokenized-payment-source.request"; +interface TokenizedPaymentMethod { + type: "bankAccount" | "card" | "payPal"; + token: string; +} + +interface BillingAddress { + country: string; + postalCode: string; + line1: string | null; + line2: string | null; + city: string | null; + state: string | null; + taxId: { code: string; value: string } | null; +} export class ProviderSetupRequest { name: string; @@ -9,6 +21,6 @@ export class ProviderSetupRequest { billingEmail: string; token: string; key: string; - taxInfo: ExpandedTaxInfoUpdateRequest; - paymentSource?: TokenizedPaymentSourceRequest; + paymentMethod: TokenizedPaymentMethod; + billingAddress: BillingAddress; } diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index 598bb2a29db..6a7b71389bb 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -7,21 +7,17 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; -import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request"; import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request"; import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; -import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; -import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response"; import { ImportDirectoryRequest } from "../../../models/request/import-directory.request"; import { SeatRequest } from "../../../models/request/seat.request"; import { StorageRequest } from "../../../models/request/storage.request"; -import { VerifyBankRequest } from "../../../models/request/verify-bank.request"; import { ListResponse } from "../../../models/response/list.response"; import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction"; @@ -143,10 +139,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return data; } - async updatePayment(id: string, request: PaymentRequest): Promise { - return this.apiService.send("POST", "/organizations/" + id + "/payment", request, true, false); - } - async upgrade(id: string, request: OrganizationUpgradeRequest): Promise { const r = await this.apiService.send( "POST", @@ -208,16 +200,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return new PaymentResponse(r); } - async verifyBank(id: string, request: VerifyBankRequest): Promise { - await this.apiService.send( - "POST", - "/organizations/" + id + "/verify-bank", - request, - true, - false, - ); - } - async reinstate(id: string): Promise { return this.apiService.send("POST", "/organizations/" + id + "/reinstate", null, true, false); } @@ -299,16 +281,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return new ApiKeyResponse(r); } - async getTaxInfo(id: string): Promise { - const r = await this.apiService.send("GET", "/organizations/" + id + "/tax", null, true, true); - return new TaxInfoResponse(r); - } - - async updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise { - // Can't broadcast anything because the response doesn't have content - return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false); - } - async getKeys(id: string): Promise { const r = await this.apiService.send("GET", "/organizations/" + id + "/keys", null, true, true); return new OrganizationKeysResponse(r); diff --git a/libs/common/src/auth/send-access/abstractions/index.ts b/libs/common/src/auth/send-access/abstractions/index.ts new file mode 100644 index 00000000000..be8d6282020 --- /dev/null +++ b/libs/common/src/auth/send-access/abstractions/index.ts @@ -0,0 +1 @@ +export * from "./send-token.service"; diff --git a/libs/common/src/auth/send-access/abstractions/send-token.service.ts b/libs/common/src/auth/send-access/abstractions/send-token.service.ts new file mode 100644 index 00000000000..3ecdc101892 --- /dev/null +++ b/libs/common/src/auth/send-access/abstractions/send-token.service.ts @@ -0,0 +1,57 @@ +import { Observable } from "rxjs"; + +import { SendAccessToken } from "../models/send-access-token"; +import { GetSendAccessTokenError } from "../types/get-send-access-token-error.type"; +import { SendAccessDomainCredentials } from "../types/send-access-domain-credentials.type"; +import { SendHashedPasswordB64 } from "../types/send-hashed-password-b64.type"; +import { TryGetSendAccessTokenError } from "../types/try-get-send-access-token-error.type"; + +/** + * Service to manage send access tokens. + */ +export abstract class SendTokenService { + /** + * Attempts to retrieve a {@link SendAccessToken} for the given sendId. + * If the access token is found in session storage and is not expired, then it returns the token. + * If the access token is expired, then it returns a {@link TryGetSendAccessTokenError} expired error. + * If an access token is not found in storage, then it attempts to retrieve it from the server (will succeed for sends that don't require any credentials to view). + * If the access token is successfully retrieved from the server, then it stores the token in session storage and returns it. + * If an access token cannot be granted b/c the send requires credentials, then it returns a {@link TryGetSendAccessTokenError} indicating which credentials are required. + * Any submissions of credentials will be handled by the getSendAccessToken$ method. + * @param sendId The ID of the send to retrieve the access token for. + * @returns An observable that emits a SendAccessToken if successful, or a TryGetSendAccessTokenError if not. + */ + abstract tryGetSendAccessToken$: ( + sendId: string, + ) => Observable; + + /** + * Retrieves a SendAccessToken for the given sendId using the provided credentials. + * If the access token is successfully retrieved from the server, it stores the token in session storage and returns it. + * If the access token cannot be granted due to invalid credentials, it returns a {@link GetSendAccessTokenError}. + * @param sendId The ID of the send to retrieve the access token for. + * @param sendAccessCredentials The credentials to use for accessing the send. + * @returns An observable that emits a SendAccessToken if successful, or a GetSendAccessTokenError if not. + */ + abstract getSendAccessToken$: ( + sendId: string, + sendAccessCredentials: SendAccessDomainCredentials, + ) => Observable; + + /** + * Hashes a password for send access which is required to create a {@link SendAccessTokenRequest} + * (more specifically, to create a {@link SendAccessDomainCredentials} for sends that require a password) + * @param password The raw password string to hash. + * @param keyMaterialUrlB64 The base64 URL encoded key material string. + * @returns A promise that resolves to the hashed password as a SendHashedPasswordB64. + */ + abstract hashSendPassword: ( + password: string, + keyMaterialUrlB64: string, + ) => Promise; + + /** + * Clears a send access token from storage. + */ + abstract invalidateSendAccessToken: (sendId: string) => Promise; +} diff --git a/libs/common/src/auth/send-access/index.ts b/libs/common/src/auth/send-access/index.ts new file mode 100644 index 00000000000..38352451e49 --- /dev/null +++ b/libs/common/src/auth/send-access/index.ts @@ -0,0 +1,4 @@ +export * from "./abstractions"; +export * from "./models"; +export * from "./services"; +export * from "./types"; diff --git a/libs/common/src/auth/send-access/models/index.ts b/libs/common/src/auth/send-access/models/index.ts new file mode 100644 index 00000000000..9748d794715 --- /dev/null +++ b/libs/common/src/auth/send-access/models/index.ts @@ -0,0 +1 @@ +export * from "./send-access-token"; diff --git a/libs/common/src/auth/send-access/models/send-access-token.spec.ts b/libs/common/src/auth/send-access/models/send-access-token.spec.ts new file mode 100644 index 00000000000..6bacac7eb6d --- /dev/null +++ b/libs/common/src/auth/send-access/models/send-access-token.spec.ts @@ -0,0 +1,75 @@ +import { SendAccessTokenResponse } from "@bitwarden/sdk-internal"; + +import { SendAccessToken } from "./send-access-token"; + +describe("SendAccessToken", () => { + const sendId = "sendId"; + + const NOW = 1_000_000; // fixed timestamp for predictable results + + const expiresAt: number = NOW + 1000 * 60 * 5; // 5 minutes from now + + const expiredExpiresAt: number = NOW - 1000 * 60 * 5; // 5 minutes ago + + let nowSpy: jest.SpyInstance; + + beforeAll(() => { + nowSpy = jest.spyOn(Date, "now"); + }); + + beforeEach(() => { + // Ensure every test starts from the same fixed time + nowSpy.mockReturnValue(NOW); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("should create a valid, unexpired token", () => { + const token = new SendAccessToken(sendId, expiresAt); + expect(token).toBeTruthy(); + expect(token.isExpired()).toBe(false); + }); + + it("should be expired after the expiration time", () => { + const token = new SendAccessToken(sendId, expiredExpiresAt); + expect(token.isExpired()).toBe(true); + }); + + it("should be considered expired if within 5 seconds of expiration", () => { + const token = new SendAccessToken(sendId, expiresAt); + nowSpy.mockReturnValue(expiresAt - 4_000); // 4 seconds before expiry + expect(token.isExpired()).toBe(true); + }); + + it("should return the correct time until expiry in seconds", () => { + const token = new SendAccessToken(sendId, expiresAt); + expect(token.timeUntilExpirySeconds()).toBe(300); // 5 minutes + }); + + it("should return 0 if the token is expired", () => { + const token = new SendAccessToken(sendId, expiredExpiresAt); + expect(token.timeUntilExpirySeconds()).toBe(0); + }); + + it("should create a token from JSON", () => { + const json = { + token: sendId, + expiresAt: expiresAt, + }; + const token = SendAccessToken.fromJson(json); + expect(token).toBeTruthy(); + expect(token.isExpired()).toBe(false); + }); + + it("should create a token from SendAccessTokenResponse", () => { + const response = { + token: sendId, + expiresAt: expiresAt, + } as SendAccessTokenResponse; + const token = SendAccessToken.fromSendAccessTokenResponse(response); + expect(token).toBeTruthy(); + expect(token.isExpired()).toBe(false); + }); +}); diff --git a/libs/common/src/auth/send-access/models/send-access-token.ts b/libs/common/src/auth/send-access/models/send-access-token.ts new file mode 100644 index 00000000000..e237243159d --- /dev/null +++ b/libs/common/src/auth/send-access/models/send-access-token.ts @@ -0,0 +1,46 @@ +import { Jsonify } from "type-fest"; + +import { SendAccessTokenResponse } from "@bitwarden/sdk-internal"; + +export class SendAccessToken { + constructor( + /** + * The access token string + */ + readonly token: string, + /** + * The time (in milliseconds since the epoch) when the token expires + */ + readonly expiresAt: number, + ) {} + + /** Returns whether the send access token is expired or not + * Has a 5 second threshold to avoid race conditions with the token + * expiring in flight + */ + isExpired(threshold: number = 5_000): boolean { + return Date.now() >= this.expiresAt - threshold; + } + + /** Returns how many full seconds remain until expiry. Returns 0 if expired. */ + timeUntilExpirySeconds(): number { + return Math.max(0, Math.floor((this.expiresAt - Date.now()) / 1_000)); + } + + static fromJson(parsedJson: Jsonify): SendAccessToken { + return new SendAccessToken(parsedJson.token, parsedJson.expiresAt); + } + + /** + * Creates a SendAccessToken from a SendAccessTokenResponse. + * @param sendAccessTokenResponse The SDK response object containing the token and expiry information. + * @returns A new instance of SendAccessToken. + * note: we need to convert from the SDK response type to our internal type so that we can + * be sure it will serialize/deserialize correctly in state provider. + */ + static fromSendAccessTokenResponse( + sendAccessTokenResponse: SendAccessTokenResponse, + ): SendAccessToken { + return new SendAccessToken(sendAccessTokenResponse.token, sendAccessTokenResponse.expiresAt); + } +} diff --git a/libs/common/src/auth/send-access/services/default-send-token.service.spec.ts b/libs/common/src/auth/send-access/services/default-send-token.service.spec.ts new file mode 100644 index 00000000000..8db0532911f --- /dev/null +++ b/libs/common/src/auth/send-access/services/default-send-token.service.spec.ts @@ -0,0 +1,678 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; + +import { + SendAccessTokenApiErrorResponse, + SendAccessTokenError, + SendAccessTokenInvalidGrantError, + SendAccessTokenInvalidRequestError, + SendAccessTokenResponse, + UnexpectedIdentityError, +} from "@bitwarden/sdk-internal"; +import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/state-test-utils"; + +import { + SendHashedPassword, + SendPasswordKeyMaterial, + SendPasswordService, +} from "../../../key-management/sends"; +import { Utils } from "../../../platform/misc/utils"; +import { MockSdkService } from "../../../platform/spec/mock-sdk.service"; +import { SendAccessToken } from "../models/send-access-token"; +import { GetSendAccessTokenError } from "../types/get-send-access-token-error.type"; +import { SendAccessDomainCredentials } from "../types/send-access-domain-credentials.type"; +import { SendHashedPasswordB64 } from "../types/send-hashed-password-b64.type"; +import { SendOtp } from "../types/send-otp.type"; + +import { DefaultSendTokenService } from "./default-send-token.service"; +import { SEND_ACCESS_TOKEN_DICT } from "./send-access-token-dict.state"; + +describe("SendTokenService", () => { + let service: DefaultSendTokenService; + + // Deps + let sdkService: MockSdkService; + let globalStateProvider: FakeGlobalStateProvider; + let sendPasswordService: MockProxy; + + beforeEach(() => { + globalStateProvider = new FakeGlobalStateProvider(); + sdkService = new MockSdkService(); + sendPasswordService = mock(); + + service = new DefaultSendTokenService(globalStateProvider, sdkService, sendPasswordService); + }); + + it("instantiates", () => { + expect(service).toBeTruthy(); + }); + + describe("Send access token retrieval tests", () => { + let sendAccessTokenDictGlobalState: FakeGlobalState>; + + let sendAccessTokenResponse: SendAccessTokenResponse; + + let sendId: string; + let sendAccessToken: SendAccessToken; + let token: string; + let tokenExpiresAt: number; + + const EXPECTED_SERVER_KIND: GetSendAccessTokenError["kind"] = "expected_server"; + const UNEXPECTED_SERVER_KIND: GetSendAccessTokenError["kind"] = "unexpected_server"; + + const INVALID_REQUEST_CODES: SendAccessTokenInvalidRequestError[] = [ + "send_id_required", + "password_hash_b64_required", + "email_required", + "email_and_otp_required_otp_sent", + "unknown", + ]; + + const INVALID_GRANT_CODES: SendAccessTokenInvalidGrantError[] = [ + "send_id_invalid", + "password_hash_b64_invalid", + "email_invalid", + "otp_invalid", + "otp_generation_failed", + "unknown", + ]; + + const CREDS = [ + { kind: "password", passwordHashB64: "h4sh" as SendHashedPasswordB64 }, + { kind: "email", email: "u@example.com" }, + { kind: "email_otp", email: "u@example.com", otp: "123456" as SendOtp }, + ] as const satisfies readonly SendAccessDomainCredentials[]; + + type SendAccessTokenApiErrorResponseErrorCode = SendAccessTokenApiErrorResponse["error"]; + + type SimpleErrorType = Exclude< + SendAccessTokenApiErrorResponseErrorCode, + "invalid_request" | "invalid_grant" + >; + + // Extract out simple error types which don't have complex send_access_error_types to handle. + const SIMPLE_ERROR_TYPES = [ + "invalid_client", + "unauthorized_client", + "unsupported_grant_type", + "invalid_scope", + "invalid_target", + ] as const satisfies readonly SimpleErrorType[]; + + beforeEach(() => { + sendId = "sendId"; + token = "sendAccessToken"; + tokenExpiresAt = Date.now() + 1000 * 60 * 5; // 5 minutes from now + + sendAccessTokenResponse = { + token: token, + expiresAt: tokenExpiresAt, + }; + + sendAccessToken = SendAccessToken.fromSendAccessTokenResponse(sendAccessTokenResponse); + + sendAccessTokenDictGlobalState = globalStateProvider.getFake(SEND_ACCESS_TOKEN_DICT); + // Ensure the state is empty before each test + sendAccessTokenDictGlobalState.stateSubject.next({}); + }); + + describe("tryGetSendAccessToken$", () => { + it("returns the send access token from session storage when token exists and isn't expired", async () => { + // Arrange + // Store the send access token in the global state + sendAccessTokenDictGlobalState.stateSubject.next({ [sendId]: sendAccessToken }); + + // Act + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + // Assert + expect(result).toEqual(sendAccessToken); + }); + + it("returns expired error and clears token from storage when token is expired", async () => { + // Arrange + const oldDate = new Date("2025-01-01"); + const expiredSendAccessToken = new SendAccessToken(token, oldDate.getTime()); + sendAccessTokenDictGlobalState.stateSubject.next({ [sendId]: expiredSendAccessToken }); + + // Act + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + // Assert + expect(result).not.toBeInstanceOf(SendAccessToken); + expect(result).toStrictEqual({ kind: "expired" }); + + // assert that we removed the expired token from storage. + const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$); + expect(sendAccessTokenDict).not.toHaveProperty(sendId); + }); + + it("calls to get a new token if none is found in storage and stores the retrieved token in session storage", async () => { + // Arrange + sdkService.client.auth + .mockDeep() + .send_access.mockDeep() + .request_send_access_token.mockResolvedValue(sendAccessTokenResponse); + + // Act + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + // Assert + expect(result).toBeInstanceOf(SendAccessToken); + expect(result).toEqual(sendAccessToken); + const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$); + expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken); + }); + + describe("handles expected invalid_request scenarios appropriately", () => { + it.each(INVALID_REQUEST_CODES)( + "surfaces %s as an expected invalid_request error", + async (code) => { + // Arrange + const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = { + error: "invalid_request", + error_description: code, + send_access_error_type: code, + }; + mockSdkRejectWith({ + kind: "expected", + data: sendAccessTokenApiErrorResponse, + }); + + // Act + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + // Assert + expect(result).toEqual({ + kind: EXPECTED_SERVER_KIND, + error: sendAccessTokenApiErrorResponse, + }); + }, + ); + + it("handles bare expected invalid_request scenario appropriately", async () => { + const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = { + error: "invalid_request", + }; + mockSdkRejectWith({ + kind: "expected", + data: sendAccessTokenApiErrorResponse, + }); + + // Act + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + // Assert + expect(result).toEqual({ + kind: EXPECTED_SERVER_KIND, + error: sendAccessTokenApiErrorResponse, + }); + }); + }); + + it.each(SIMPLE_ERROR_TYPES)("handles expected %s error appropriately", async (errorType) => { + const api: SendAccessTokenApiErrorResponse = { + error: errorType, + error_description: `The ${errorType} error occurred`, + }; + mockSdkRejectWith({ kind: "expected", data: api }); + + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + expect(result).toEqual({ kind: EXPECTED_SERVER_KIND, error: api }); + }); + + it.each(SIMPLE_ERROR_TYPES)( + "handles expected %s bare error appropriately", + async (errorType) => { + const api: SendAccessTokenApiErrorResponse = { error: errorType }; + mockSdkRejectWith({ kind: "expected", data: api }); + + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + expect(result).toEqual({ kind: EXPECTED_SERVER_KIND, error: api }); + }, + ); + + describe("handles expected invalid_grant scenarios appropriately", () => { + it.each(INVALID_GRANT_CODES)( + "surfaces %s as an expected invalid_grant error", + async (code) => { + // Arrange + const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = { + error: "invalid_grant", + error_description: code, + send_access_error_type: code, + }; + mockSdkRejectWith({ + kind: "expected", + data: sendAccessTokenApiErrorResponse, + }); + + // Act + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + // Assert + expect(result).toEqual({ + kind: EXPECTED_SERVER_KIND, + error: sendAccessTokenApiErrorResponse, + }); + }, + ); + + it("handles bare expected invalid_grant scenario appropriately", async () => { + const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = { + error: "invalid_grant", + }; + mockSdkRejectWith({ + kind: "expected", + data: sendAccessTokenApiErrorResponse, + }); + + // Act + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + // Assert + expect(result).toEqual({ + kind: EXPECTED_SERVER_KIND, + error: sendAccessTokenApiErrorResponse, + }); + }); + }); + + it("surfaces unexpected errors as unexpected_server error", async () => { + // Arrange + const unexpectedIdentityError: UnexpectedIdentityError = "unexpected error occurred"; + + mockSdkRejectWith({ + kind: "unexpected", + data: unexpectedIdentityError, + }); + + // Act + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + // Assert + expect(result).toEqual({ + kind: UNEXPECTED_SERVER_KIND, + error: unexpectedIdentityError, + }); + }); + + it("surfaces an unknown error as an unknown error", async () => { + // Arrange + const unknownError = "unknown error occurred"; + + sdkService.client.auth + .mockDeep() + .send_access.mockDeep() + .request_send_access_token.mockRejectedValue(new Error(unknownError)); + + // Act + const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId)); + + // Assert + expect(result).toEqual({ + kind: "unknown", + error: unknownError, + }); + }); + + describe("getSendAccessTokenFromStorage", () => { + it("returns undefined if no token is found in storage", async () => { + const result = await (service as any).getSendAccessTokenFromStorage("nonexistentSendId"); + expect(result).toBeUndefined(); + }); + + it("returns the token if found in storage", async () => { + sendAccessTokenDictGlobalState.stateSubject.next({ [sendId]: sendAccessToken }); + const result = await (service as any).getSendAccessTokenFromStorage(sendId); + expect(result).toEqual(sendAccessToken); + }); + + it("returns undefined if the global state isn't initialized yet", async () => { + sendAccessTokenDictGlobalState.stateSubject.next(null); + + const result = await (service as any).getSendAccessTokenFromStorage(sendId); + expect(result).toBeUndefined(); + }); + }); + + describe("setSendAccessTokenInStorage", () => { + it("stores the token in storage", async () => { + await (service as any).setSendAccessTokenInStorage(sendId, sendAccessToken); + const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$); + expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken); + }); + + it("initializes the dictionary if it isn't already", async () => { + sendAccessTokenDictGlobalState.stateSubject.next(null); + + await (service as any).setSendAccessTokenInStorage(sendId, sendAccessToken); + const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$); + expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken); + }); + + it("merges with existing tokens in storage", async () => { + const anotherSendId = "anotherSendId"; + const anotherSendAccessToken = new SendAccessToken( + "anotherToken", + Date.now() + 1000 * 60, + ); + + sendAccessTokenDictGlobalState.stateSubject.next({ + [anotherSendId]: anotherSendAccessToken, + }); + await (service as any).setSendAccessTokenInStorage(sendId, sendAccessToken); + const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$); + expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken); + expect(sendAccessTokenDict).toHaveProperty(anotherSendId, anotherSendAccessToken); + }); + }); + }); + + describe("getSendAccessToken$", () => { + it("returns a send access token for a password protected send when given valid password credentials", async () => { + // Arrange + const sendPasswordCredentials: SendAccessDomainCredentials = { + kind: "password", + passwordHashB64: "testPassword" as SendHashedPasswordB64, + }; + + sdkService.client.auth + .mockDeep() + .send_access.mockDeep() + .request_send_access_token.mockResolvedValue(sendAccessTokenResponse); + + // Act + const result = await firstValueFrom( + service.getSendAccessToken$(sendId, sendPasswordCredentials), + ); + + // Assert + expect(result).toEqual(sendAccessToken); + + const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$); + expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken); + }); + + // Note: we deliberately aren't testing the "success" scenario of passing + // just SendEmailCredentials as that will never return a send access token on it's own. + + it("returns a send access token for a email + otp protected send when given valid email and otp", async () => { + // Arrange + const sendEmailOtpCredentials: SendAccessDomainCredentials = { + kind: "email_otp", + email: "test@example.com", + otp: "123456" as SendOtp, + }; + + sdkService.client.auth + .mockDeep() + .send_access.mockDeep() + .request_send_access_token.mockResolvedValue(sendAccessTokenResponse); + + // Act + const result = await firstValueFrom( + service.getSendAccessToken$(sendId, sendEmailOtpCredentials), + ); + + // Assert + expect(result).toEqual(sendAccessToken); + const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$); + expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken); + }); + + describe.each(CREDS.map((c) => [c.kind, c] as const))( + "scenarios with %s credentials", + (_label, creds) => { + it.each(INVALID_REQUEST_CODES)( + "handles expected invalid_request.%s scenario appropriately", + async (code) => { + const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = { + error: "invalid_request", + error_description: code, + send_access_error_type: code, + }; + + mockSdkRejectWith({ + kind: "expected", + data: sendAccessTokenApiErrorResponse, + }); + + const result = await firstValueFrom(service.getSendAccessToken$("abc123", creds)); + + expect(result).toEqual({ + kind: "expected_server", + error: sendAccessTokenApiErrorResponse, + }); + }, + ); + + it("handles expected invalid_request scenario appropriately", async () => { + const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = { + error: "invalid_request", + }; + mockSdkRejectWith({ + kind: "expected", + data: sendAccessTokenApiErrorResponse, + }); + + // Act + const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds)); + + // Assert + expect(result).toEqual({ + kind: "expected_server", + error: sendAccessTokenApiErrorResponse, + }); + }); + + it.each(INVALID_GRANT_CODES)( + "handles expected invalid_grant.%s scenario appropriately", + async (code) => { + const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = { + error: "invalid_grant", + error_description: code, + send_access_error_type: code, + }; + + mockSdkRejectWith({ + kind: "expected", + data: sendAccessTokenApiErrorResponse, + }); + + const result = await firstValueFrom(service.getSendAccessToken$("abc123", creds)); + + expect(result).toEqual({ + kind: "expected_server", + error: sendAccessTokenApiErrorResponse, + }); + }, + ); + + it("handles expected invalid_grant scenario appropriately", async () => { + const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = { + error: "invalid_grant", + }; + mockSdkRejectWith({ + kind: "expected", + data: sendAccessTokenApiErrorResponse, + }); + + // Act + const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds)); + + // Assert + expect(result).toEqual({ + kind: "expected_server", + error: sendAccessTokenApiErrorResponse, + }); + }); + + it.each(SIMPLE_ERROR_TYPES)( + "handles expected %s error appropriately", + async (errorType) => { + const api: SendAccessTokenApiErrorResponse = { + error: errorType, + error_description: `The ${errorType} error occurred`, + }; + mockSdkRejectWith({ kind: "expected", data: api }); + + const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds)); + + expect(result).toEqual({ kind: EXPECTED_SERVER_KIND, error: api }); + }, + ); + + it.each(SIMPLE_ERROR_TYPES)( + "handles expected %s bare error appropriately", + async (errorType) => { + const api: SendAccessTokenApiErrorResponse = { error: errorType }; + mockSdkRejectWith({ kind: "expected", data: api }); + + const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds)); + + expect(result).toEqual({ kind: EXPECTED_SERVER_KIND, error: api }); + }, + ); + + it("surfaces unexpected errors as unexpected_server error", async () => { + // Arrange + const unexpectedIdentityError: UnexpectedIdentityError = "unexpected error occurred"; + + mockSdkRejectWith({ + kind: "unexpected", + data: unexpectedIdentityError, + }); + + // Act + const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds)); + + // Assert + expect(result).toEqual({ + kind: UNEXPECTED_SERVER_KIND, + error: unexpectedIdentityError, + }); + }); + + it("surfaces an unknown error as an unknown error", async () => { + // Arrange + const unknownError = "unknown error occurred"; + + sdkService.client.auth + .mockDeep() + .send_access.mockDeep() + .request_send_access_token.mockRejectedValue(new Error(unknownError)); + + // Act + const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds)); + + // Assert + expect(result).toEqual({ + kind: "unknown", + error: unknownError, + }); + }); + }, + ); + + it("errors if passwordHashB64 is missing for password credentials", async () => { + const creds: SendAccessDomainCredentials = { + kind: "password", + passwordHashB64: "" as SendHashedPasswordB64, + }; + await expect(firstValueFrom(service.getSendAccessToken$(sendId, creds))).rejects.toThrow( + "passwordHashB64 must be provided for password credentials.", + ); + }); + + it("errors if email is missing for email credentials", async () => { + const creds: SendAccessDomainCredentials = { + kind: "email", + email: "", + }; + await expect(firstValueFrom(service.getSendAccessToken$(sendId, creds))).rejects.toThrow( + "email must be provided for email credentials.", + ); + }); + + it("errors if email or otp is missing for email_otp credentials", async () => { + const creds: SendAccessDomainCredentials = { + kind: "email_otp", + email: "", + otp: "" as SendOtp, + }; + await expect(firstValueFrom(service.getSendAccessToken$(sendId, creds))).rejects.toThrow( + "email and otp must be provided for email_otp credentials.", + ); + }); + }); + + describe("invalidateSendAccessToken", () => { + it("removes a send access token from storage", async () => { + // Arrange + sendAccessTokenDictGlobalState.stateSubject.next({ [sendId]: sendAccessToken }); + + // Act + await service.invalidateSendAccessToken(sendId); + const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$); + + // Assert + expect(sendAccessTokenDict).not.toHaveProperty(sendId); + }); + }); + }); + + describe("hashSendPassword", () => { + test.each(["", null, undefined])("rejects if password is %p", async (pwd) => { + await expect(service.hashSendPassword(pwd as any, "keyMaterialUrlB64")).rejects.toThrow( + "Password must be provided.", + ); + }); + + test.each(["", null, undefined])( + "rejects if keyMaterialUrlB64 is %p", + async (keyMaterialUrlB64) => { + await expect( + service.hashSendPassword("password", keyMaterialUrlB64 as any), + ).rejects.toThrow("KeyMaterialUrlB64 must be provided."); + }, + ); + + it("correctly hashes the password", async () => { + // Arrange + const password = "testPassword"; + const keyMaterialUrlB64 = "testKeyMaterialUrlB64"; + const keyMaterialArray = new Uint8Array([1, 2, 3]) as SendPasswordKeyMaterial; + const hashedPasswordArray = new Uint8Array([4, 5, 6]) as SendHashedPassword; + const sendHashedPasswordB64 = "hashedPasswordB64" as SendHashedPasswordB64; + + const utilsFromUrlB64ToArraySpy = jest + .spyOn(Utils, "fromUrlB64ToArray") + .mockReturnValue(keyMaterialArray); + + sendPasswordService.hashPassword.mockResolvedValue(hashedPasswordArray); + + const utilsFromBufferToB64Spy = jest + .spyOn(Utils, "fromBufferToB64") + .mockReturnValue(sendHashedPasswordB64); + + // Act + const result = await service.hashSendPassword(password, keyMaterialUrlB64); + + // Assert + expect(sendPasswordService.hashPassword).toHaveBeenCalledWith(password, keyMaterialArray); + expect(utilsFromUrlB64ToArraySpy).toHaveBeenCalledWith(keyMaterialUrlB64); + expect(utilsFromBufferToB64Spy).toHaveBeenCalledWith(hashedPasswordArray); + expect(result).toBe(sendHashedPasswordB64); + }); + }); + + function mockSdkRejectWith(sendAccessTokenError: SendAccessTokenError) { + sdkService.client.auth + .mockDeep() + .send_access.mockDeep() + .request_send_access_token.mockRejectedValue(sendAccessTokenError); + } +}); diff --git a/libs/common/src/auth/send-access/services/default-send-token.service.ts b/libs/common/src/auth/send-access/services/default-send-token.service.ts new file mode 100644 index 00000000000..4d3376fd5b6 --- /dev/null +++ b/libs/common/src/auth/send-access/services/default-send-token.service.ts @@ -0,0 +1,316 @@ +import { Observable, defer, firstValueFrom, from } from "rxjs"; + +import { + BitwardenClient, + SendAccessCredentials, + SendAccessTokenError, + SendAccessTokenRequest, + SendAccessTokenResponse, +} from "@bitwarden/sdk-internal"; +import { GlobalState, GlobalStateProvider } from "@bitwarden/state"; + +import { SendPasswordService } from "../../../key-management/sends/abstractions/send-password.service"; +import { + SendHashedPassword, + SendPasswordKeyMaterial, +} from "../../../key-management/sends/types/send-hashed-password.type"; +import { SdkService } from "../../../platform/abstractions/sdk/sdk.service"; +import { Utils } from "../../../platform/misc/utils"; +import { SendTokenService as SendTokenServiceAbstraction } from "../abstractions/send-token.service"; +import { SendAccessToken } from "../models/send-access-token"; +import { GetSendAccessTokenError } from "../types/get-send-access-token-error.type"; +import { SendAccessDomainCredentials } from "../types/send-access-domain-credentials.type"; +import { SendHashedPasswordB64 } from "../types/send-hashed-password-b64.type"; +import { TryGetSendAccessTokenError } from "../types/try-get-send-access-token-error.type"; + +import { SEND_ACCESS_TOKEN_DICT } from "./send-access-token-dict.state"; + +export class DefaultSendTokenService implements SendTokenServiceAbstraction { + private sendAccessTokenDictGlobalState: GlobalState> | undefined; + + constructor( + private globalStateProvider: GlobalStateProvider, + private sdkService: SdkService, + private sendPasswordService: SendPasswordService, + ) { + this.initializeState(); + } + + private initializeState(): void { + this.sendAccessTokenDictGlobalState = this.globalStateProvider.get(SEND_ACCESS_TOKEN_DICT); + } + + tryGetSendAccessToken$(sendId: string): Observable { + // Defer the execution to ensure that a cold observable is returned. + return defer(() => from(this._tryGetSendAccessToken(sendId))); + } + + private async _tryGetSendAccessToken( + sendId: string, + ): Promise { + // Validate the sendId is a non-empty string. + this.validateSendId(sendId); + + // Check in storage for the access token for the given sendId. + const sendAccessTokenFromStorage = await this.getSendAccessTokenFromStorage(sendId); + + if (sendAccessTokenFromStorage != null) { + // If it is expired, we clear the token from storage and return the expired error + if (sendAccessTokenFromStorage.isExpired()) { + await this.clearSendAccessTokenFromStorage(sendId); + return { kind: "expired" }; + } else { + // If it is not expired, we return it + return sendAccessTokenFromStorage; + } + } + + // If we don't have a token in storage, we can try to request a new token from the server. + const request: SendAccessTokenRequest = { + sendId: sendId, + }; + + const anonSdkClient: BitwardenClient = await firstValueFrom(this.sdkService.client$); + + try { + const result: SendAccessTokenResponse = await anonSdkClient + .auth() + .send_access() + .request_send_access_token(request); + + // Convert from SDK shape to SendAccessToken so it can be serialized into & out of state provider + const sendAccessToken = SendAccessToken.fromSendAccessTokenResponse(result); + + // If we get a token back, we need to store it in the global state. + await this.setSendAccessTokenInStorage(sendId, sendAccessToken); + + return sendAccessToken; + } catch (error: unknown) { + return this.normalizeSendAccessTokenError(error); + } + } + + getSendAccessToken$( + sendId: string, + sendCredentials: SendAccessDomainCredentials, + ): Observable { + // Defer the execution to ensure that a cold observable is returned. + return defer(() => from(this._getSendAccessToken(sendId, sendCredentials))); + } + + private async _getSendAccessToken( + sendId: string, + sendAccessCredentials: SendAccessDomainCredentials, + ): Promise { + // Validate inputs to account for non-strict TS call sites. + this.validateCredentialsRequest(sendId, sendAccessCredentials); + + // Convert inputs to SDK request shape + const request: SendAccessTokenRequest = { + sendId: sendId, + sendAccessCredentials: this.convertDomainCredentialsToSdkCredentials(sendAccessCredentials), + }; + + const anonSdkClient: BitwardenClient = await firstValueFrom(this.sdkService.client$); + + try { + const result: SendAccessTokenResponse = await anonSdkClient + .auth() + .send_access() + .request_send_access_token(request); + + // Convert from SDK interface to SendAccessToken class so it can be serialized into & out of state provider + const sendAccessToken = SendAccessToken.fromSendAccessTokenResponse(result); + + // Any time we get a token from the server, we need to store it in the global state. + await this.setSendAccessTokenInStorage(sendId, sendAccessToken); + + return sendAccessToken; + } catch (error: unknown) { + return this.normalizeSendAccessTokenError(error); + } + } + + async invalidateSendAccessToken(sendId: string): Promise { + await this.clearSendAccessTokenFromStorage(sendId); + } + + async hashSendPassword( + password: string, + keyMaterialUrlB64: string, + ): Promise { + // Validate the password and key material + if (password == null || password.trim() === "") { + throw new Error("Password must be provided."); + } + if (keyMaterialUrlB64 == null || keyMaterialUrlB64.trim() === "") { + throw new Error("KeyMaterialUrlB64 must be provided."); + } + + // Convert the base64 URL encoded key material to a Uint8Array + const keyMaterialUrlB64Array = Utils.fromUrlB64ToArray( + keyMaterialUrlB64, + ) as SendPasswordKeyMaterial; + + const sendHashedPasswordArray: SendHashedPassword = await this.sendPasswordService.hashPassword( + password, + keyMaterialUrlB64Array, + ); + + // Convert the Uint8Array to a base64 encoded string which is required + // for the server to be able to compare the password hash. + const sendHashedPasswordB64 = Utils.fromBufferToB64( + sendHashedPasswordArray, + ) as SendHashedPasswordB64; + + return sendHashedPasswordB64; + } + + private async getSendAccessTokenFromStorage( + sendId: string, + ): Promise { + if (this.sendAccessTokenDictGlobalState != null) { + const sendAccessTokenDict = await firstValueFrom(this.sendAccessTokenDictGlobalState.state$); + return sendAccessTokenDict?.[sendId]; + } + return undefined; + } + + private async setSendAccessTokenInStorage( + sendId: string, + sendAccessToken: SendAccessToken, + ): Promise { + if (this.sendAccessTokenDictGlobalState != null) { + await this.sendAccessTokenDictGlobalState.update( + (sendAccessTokenDict) => { + sendAccessTokenDict ??= {}; // Initialize if undefined + + sendAccessTokenDict[sendId] = sendAccessToken; + return sendAccessTokenDict; + }, + { + // only update if the value is different (to avoid unnecessary writes) + shouldUpdate: (prevDict) => { + const prevSendAccessToken = prevDict?.[sendId]; + return ( + prevSendAccessToken?.token !== sendAccessToken.token || + prevSendAccessToken?.expiresAt !== sendAccessToken.expiresAt + ); + }, + }, + ); + } + } + + private async clearSendAccessTokenFromStorage(sendId: string): Promise { + if (this.sendAccessTokenDictGlobalState != null) { + await this.sendAccessTokenDictGlobalState.update( + (sendAccessTokenDict) => { + if (!sendAccessTokenDict) { + // If the dict is empty or undefined, there's nothing to clear + return sendAccessTokenDict; + } + + if (sendAccessTokenDict[sendId] == null) { + // If the specific sendId does not exist, nothing to clear + return sendAccessTokenDict; + } + + // Destructure to omit the specific sendId and get new reference for the rest of the dict for an immutable update + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [sendId]: _, ...rest } = sendAccessTokenDict; + + return rest; + }, + { + // only update if the value is defined (to avoid unnecessary writes) + shouldUpdate: (prevDict) => prevDict?.[sendId] != null, + }, + ); + } + } + + /** + * Normalizes an error from the SDK send access token request process. + * @param e The error to normalize. + * @returns A normalized GetSendAccessTokenError. + */ + private normalizeSendAccessTokenError(e: unknown): GetSendAccessTokenError { + if (this.isSendAccessTokenError(e)) { + if (e.kind === "unexpected") { + return { kind: "unexpected_server", error: e.data }; + } + return { kind: "expected_server", error: e.data }; + } + + if (e instanceof Error) { + return { kind: "unknown", error: e.message }; + } + + try { + return { kind: "unknown", error: JSON.stringify(e) }; + } catch { + return { kind: "unknown", error: "error cannot be stringified" }; + } + } + + private isSendAccessTokenError(e: unknown): e is SendAccessTokenError { + return ( + typeof e === "object" && + e !== null && + "kind" in e && + (e.kind === "expected" || e.kind === "unexpected") + ); + } + + private validateSendId(sendId: string): void { + if (sendId == null || sendId.trim() === "") { + throw new Error("sendId must be provided."); + } + } + + private validateCredentialsRequest( + sendId: string, + sendAccessCredentials: SendAccessDomainCredentials, + ): void { + this.validateSendId(sendId); + if (sendAccessCredentials == null) { + throw new Error("sendAccessCredentials must be provided."); + } + + if (sendAccessCredentials.kind === "password" && !sendAccessCredentials.passwordHashB64) { + throw new Error("passwordHashB64 must be provided for password credentials."); + } + + if (sendAccessCredentials.kind === "email" && !sendAccessCredentials.email) { + throw new Error("email must be provided for email credentials."); + } + + if ( + sendAccessCredentials.kind === "email_otp" && + (!sendAccessCredentials.email || !sendAccessCredentials.otp) + ) { + throw new Error("email and otp must be provided for email_otp credentials."); + } + } + + private convertDomainCredentialsToSdkCredentials( + sendAccessCredentials: SendAccessDomainCredentials, + ): SendAccessCredentials { + switch (sendAccessCredentials.kind) { + case "password": + return { + passwordHashB64: sendAccessCredentials.passwordHashB64, + }; + case "email": + return { + email: sendAccessCredentials.email, + }; + case "email_otp": + return { + email: sendAccessCredentials.email, + otp: sendAccessCredentials.otp, + }; + } + } +} diff --git a/libs/common/src/auth/send-access/services/index.ts b/libs/common/src/auth/send-access/services/index.ts new file mode 100644 index 00000000000..f45ffb94bab --- /dev/null +++ b/libs/common/src/auth/send-access/services/index.ts @@ -0,0 +1 @@ +export * from "./default-send-token.service"; diff --git a/libs/common/src/auth/send-access/services/send-access-token-dict.state.ts b/libs/common/src/auth/send-access/services/send-access-token-dict.state.ts new file mode 100644 index 00000000000..77e390768fc --- /dev/null +++ b/libs/common/src/auth/send-access/services/send-access-token-dict.state.ts @@ -0,0 +1,15 @@ +import { Jsonify } from "type-fest"; + +import { KeyDefinition, SEND_ACCESS_DISK } from "@bitwarden/state"; + +import { SendAccessToken } from "../models/send-access-token"; + +export const SEND_ACCESS_TOKEN_DICT = KeyDefinition.record( + SEND_ACCESS_DISK, + "accessTokenDict", + { + deserializer: (sendAccessTokenJson: Jsonify) => { + return SendAccessToken.fromJson(sendAccessTokenJson); + }, + }, +); diff --git a/libs/common/src/auth/send-access/types/get-send-access-token-error.type.ts b/libs/common/src/auth/send-access/types/get-send-access-token-error.type.ts new file mode 100644 index 00000000000..224e982c84c --- /dev/null +++ b/libs/common/src/auth/send-access/types/get-send-access-token-error.type.ts @@ -0,0 +1,12 @@ +import { UnexpectedIdentityError, SendAccessTokenApiErrorResponse } from "@bitwarden/sdk-internal"; + +/** + * Represents the possible errors that can occur when retrieving a SendAccessToken. + * Note: for expected_server errors, see invalid-request-errors.type.ts and + * invalid-grant-errors.type.ts for type guards that identify specific + * SendAccessTokenApiErrorResponse errors + */ +export type GetSendAccessTokenError = + | { kind: "unexpected_server"; error: UnexpectedIdentityError } + | { kind: "expected_server"; error: SendAccessTokenApiErrorResponse } + | { kind: "unknown"; error: string }; diff --git a/libs/common/src/auth/send-access/types/index.ts b/libs/common/src/auth/send-access/types/index.ts new file mode 100644 index 00000000000..344ac00abbe --- /dev/null +++ b/libs/common/src/auth/send-access/types/index.ts @@ -0,0 +1,7 @@ +export * from "./try-get-send-access-token-error.type"; +export * from "./send-otp.type"; +export * from "./send-hashed-password-b64.type"; +export * from "./send-access-domain-credentials.type"; +export * from "./invalid-request-errors.type"; +export * from "./invalid-grant-errors.type"; +export * from "./get-send-access-token-error.type"; diff --git a/libs/common/src/auth/send-access/types/invalid-grant-errors.type.ts b/libs/common/src/auth/send-access/types/invalid-grant-errors.type.ts new file mode 100644 index 00000000000..befb869a89e --- /dev/null +++ b/libs/common/src/auth/send-access/types/invalid-grant-errors.type.ts @@ -0,0 +1,62 @@ +import { SendAccessTokenApiErrorResponse } from "@bitwarden/sdk-internal"; + +export type InvalidGrant = Extract; + +export function isInvalidGrant(e: SendAccessTokenApiErrorResponse): e is InvalidGrant { + return e.error === "invalid_grant"; +} + +export type BareInvalidGrant = Extract< + SendAccessTokenApiErrorResponse, + { error: "invalid_grant" } +> & { send_access_error_type?: undefined }; + +export function isBareInvalidGrant(e: SendAccessTokenApiErrorResponse): e is BareInvalidGrant { + return e.error === "invalid_grant" && e.send_access_error_type === undefined; +} + +export type SendIdInvalid = InvalidGrant & { + send_access_error_type: "send_id_invalid"; +}; +export function sendIdInvalid(e: SendAccessTokenApiErrorResponse): e is SendIdInvalid { + return e.error === "invalid_grant" && e.send_access_error_type === "send_id_invalid"; +} + +export type PasswordHashB64Invalid = InvalidGrant & { + send_access_error_type: "password_hash_b64_invalid"; +}; +export function passwordHashB64Invalid( + e: SendAccessTokenApiErrorResponse, +): e is PasswordHashB64Invalid { + return e.error === "invalid_grant" && e.send_access_error_type === "password_hash_b64_invalid"; +} + +export type EmailInvalid = InvalidGrant & { + send_access_error_type: "email_invalid"; +}; +export function emailInvalid(e: SendAccessTokenApiErrorResponse): e is EmailInvalid { + return e.error === "invalid_grant" && e.send_access_error_type === "email_invalid"; +} + +export type OtpInvalid = InvalidGrant & { + send_access_error_type: "otp_invalid"; +}; +export function otpInvalid(e: SendAccessTokenApiErrorResponse): e is OtpInvalid { + return e.error === "invalid_grant" && e.send_access_error_type === "otp_invalid"; +} + +export type OtpGenerationFailed = InvalidGrant & { + send_access_error_type: "otp_generation_failed"; +}; +export function otpGenerationFailed(e: SendAccessTokenApiErrorResponse): e is OtpGenerationFailed { + return e.error === "invalid_grant" && e.send_access_error_type === "otp_generation_failed"; +} + +export type UnknownInvalidGrant = InvalidGrant & { + send_access_error_type: "unknown"; +}; +export function isUnknownInvalidGrant( + e: SendAccessTokenApiErrorResponse, +): e is UnknownInvalidGrant { + return e.error === "invalid_grant" && e.send_access_error_type === "unknown"; +} diff --git a/libs/common/src/auth/send-access/types/invalid-request-errors.type.ts b/libs/common/src/auth/send-access/types/invalid-request-errors.type.ts new file mode 100644 index 00000000000..57a70e62586 --- /dev/null +++ b/libs/common/src/auth/send-access/types/invalid-request-errors.type.ts @@ -0,0 +1,62 @@ +import { SendAccessTokenApiErrorResponse } from "@bitwarden/sdk-internal"; + +export type InvalidRequest = Extract; + +export function isInvalidRequest(e: SendAccessTokenApiErrorResponse): e is InvalidRequest { + return e.error === "invalid_request"; +} + +export type BareInvalidRequest = Extract< + SendAccessTokenApiErrorResponse, + { error: "invalid_request" } +> & { send_access_error_type?: undefined }; + +export function isBareInvalidRequest(e: SendAccessTokenApiErrorResponse): e is BareInvalidRequest { + return e.error === "invalid_request" && e.send_access_error_type === undefined; +} + +export type SendIdRequired = InvalidRequest & { + send_access_error_type: "send_id_required"; +}; + +export function sendIdRequired(e: SendAccessTokenApiErrorResponse): e is SendIdRequired { + return e.error === "invalid_request" && e.send_access_error_type === "send_id_required"; +} + +export type PasswordHashB64Required = InvalidRequest & { + send_access_error_type: "password_hash_b64_required"; +}; + +export function passwordHashB64Required( + e: SendAccessTokenApiErrorResponse, +): e is PasswordHashB64Required { + return e.error === "invalid_request" && e.send_access_error_type === "password_hash_b64_required"; +} + +export type EmailRequired = InvalidRequest & { send_access_error_type: "email_required" }; + +export function emailRequired(e: SendAccessTokenApiErrorResponse): e is EmailRequired { + return e.error === "invalid_request" && e.send_access_error_type === "email_required"; +} + +export type EmailAndOtpRequiredEmailSent = InvalidRequest & { + send_access_error_type: "email_and_otp_required_otp_sent"; +}; + +export function emailAndOtpRequiredEmailSent( + e: SendAccessTokenApiErrorResponse, +): e is EmailAndOtpRequiredEmailSent { + return ( + e.error === "invalid_request" && e.send_access_error_type === "email_and_otp_required_otp_sent" + ); +} + +export type UnknownInvalidRequest = InvalidRequest & { + send_access_error_type: "unknown"; +}; + +export function isUnknownInvalidRequest( + e: SendAccessTokenApiErrorResponse, +): e is UnknownInvalidRequest { + return e.error === "invalid_request" && e.send_access_error_type === "unknown"; +} diff --git a/libs/common/src/auth/send-access/types/send-access-domain-credentials.type.ts b/libs/common/src/auth/send-access/types/send-access-domain-credentials.type.ts new file mode 100644 index 00000000000..966117dc64e --- /dev/null +++ b/libs/common/src/auth/send-access/types/send-access-domain-credentials.type.ts @@ -0,0 +1,11 @@ +import { SendHashedPasswordB64 } from "./send-hashed-password-b64.type"; +import { SendOtp } from "./send-otp.type"; + +/** + * The domain facing send access credentials + * Will be internally mapped to the SDK types + */ +export type SendAccessDomainCredentials = + | { kind: "password"; passwordHashB64: SendHashedPasswordB64 } + | { kind: "email"; email: string } + | { kind: "email_otp"; email: string; otp: SendOtp }; diff --git a/libs/common/src/auth/send-access/types/send-hashed-password-b64.type.ts b/libs/common/src/auth/send-access/types/send-hashed-password-b64.type.ts new file mode 100644 index 00000000000..9d55bdfb671 --- /dev/null +++ b/libs/common/src/auth/send-access/types/send-hashed-password-b64.type.ts @@ -0,0 +1,3 @@ +import { Opaque } from "type-fest"; + +export type SendHashedPasswordB64 = Opaque; diff --git a/libs/common/src/auth/send-access/types/send-otp.type.ts b/libs/common/src/auth/send-access/types/send-otp.type.ts new file mode 100644 index 00000000000..b5dfaa95ac5 --- /dev/null +++ b/libs/common/src/auth/send-access/types/send-otp.type.ts @@ -0,0 +1,3 @@ +import { Opaque } from "type-fest"; + +export type SendOtp = Opaque; diff --git a/libs/common/src/auth/send-access/types/try-get-send-access-token-error.type.ts b/libs/common/src/auth/send-access/types/try-get-send-access-token-error.type.ts new file mode 100644 index 00000000000..e82b426654c --- /dev/null +++ b/libs/common/src/auth/send-access/types/try-get-send-access-token-error.type.ts @@ -0,0 +1,7 @@ +import { GetSendAccessTokenError } from "./get-send-access-token-error.type"; + +/** + * Represents the possible errors that can occur when trying to retrieve a SendAccessToken by + * just a sendId. Extends {@link GetSendAccessTokenError}. + */ +export type TryGetSendAccessTokenError = { kind: "expired" } | GetSendAccessTokenError; diff --git a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts index 56aa1139cda..b848cb2f902 100644 --- a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts +++ b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts @@ -38,7 +38,7 @@ describe("WebAuthnLoginService", () => { // We must do this to make the mocked classes available for all the // assertCredential(...) tests. - global.PublicKeyCredential = MockPublicKeyCredential; + global.PublicKeyCredential = MockPublicKeyCredential as any; global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse; // Save the original navigator @@ -316,6 +316,10 @@ class MockPublicKeyCredential implements PublicKeyCredential { static isUserVerifyingPlatformAuthenticatorAvailable(): Promise { return Promise.resolve(false); } + + toJSON() { + throw new Error("Method not implemented."); + } } function buildCredentialAssertionOptions(): WebAuthnLoginCredentialAssertionOptionsView { diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index 2f3fe9125db..b5695e2e8a0 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -1,19 +1,12 @@ -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; - import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; import { ListResponse } from "../../models/response/list.response"; -import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; -import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request"; import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; -import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request"; -import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request"; import { InvoicesResponse } from "../models/response/invoices.response"; -import { PaymentMethodResponse } from "../models/response/payment-method.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { @@ -29,14 +22,10 @@ export abstract class BillingApiServiceAbstraction { request: CreateClientOrganizationRequest, ): Promise; - abstract createSetupIntent(paymentMethodType: PaymentMethodType): Promise; - abstract getOrganizationBillingMetadata( organizationId: string, ): Promise; - abstract getOrganizationPaymentMethod(organizationId: string): Promise; - abstract getPlans(): Promise>; abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise; @@ -49,44 +38,12 @@ export abstract class BillingApiServiceAbstraction { abstract getProviderSubscription(providerId: string): Promise; - abstract getProviderTaxInformation(providerId: string): Promise; - - abstract updateOrganizationPaymentMethod( - organizationId: string, - request: UpdatePaymentMethodRequest, - ): Promise; - - abstract updateOrganizationTaxInformation( - organizationId: string, - request: ExpandedTaxInfoUpdateRequest, - ): Promise; - abstract updateProviderClientOrganization( providerId: string, organizationId: string, request: UpdateClientOrganizationRequest, ): Promise; - abstract updateProviderPaymentMethod( - providerId: string, - request: UpdatePaymentMethodRequest, - ): Promise; - - abstract updateProviderTaxInformation( - providerId: string, - request: ExpandedTaxInfoUpdateRequest, - ): Promise; - - abstract verifyOrganizationBankAccount( - organizationId: string, - request: VerifyBankAccountRequest, - ): Promise; - - abstract verifyProviderBankAccount( - providerId: string, - request: VerifyBankAccountRequest, - ): Promise; - abstract restartSubscription( organizationId: string, request: OrganizationCreateRequest, diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 3254787457a..215fabfd955 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -3,7 +3,6 @@ import { UserId } from "@bitwarden/user-core"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; -import { PaymentSourceResponse } from "../models/response/payment-source.response"; export type OrganizationInformation = { name: string; @@ -45,8 +44,6 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - abstract getPaymentSource(organizationId: string): Promise; - abstract purchaseSubscription( subscription: SubscriptionInformation, activeUserId: UserId, diff --git a/libs/common/src/billing/abstractions/tax.service.abstraction.ts b/libs/common/src/billing/abstractions/tax.service.abstraction.ts deleted file mode 100644 index c94fbcba652..00000000000 --- a/libs/common/src/billing/abstractions/tax.service.abstraction.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CountryListItem } from "../models/domain"; -import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; -import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; -import { PreviewTaxAmountForOrganizationTrialRequest } from "../models/request/tax"; -import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; - -export abstract class TaxServiceAbstraction { - abstract getCountries(): CountryListItem[]; - - abstract isCountrySupported(country: string): Promise; - - abstract previewIndividualInvoice( - request: PreviewIndividualInvoiceRequest, - ): Promise; - - abstract previewOrganizationInvoice( - request: PreviewOrganizationInvoiceRequest, - ): Promise; - - abstract previewTaxAmountForOrganizationTrial: ( - request: PreviewTaxAmountForOrganizationTrialRequest, - ) => Promise; -} diff --git a/libs/common/src/billing/enums/bitwarden-product-type.enum.ts b/libs/common/src/billing/enums/bitwarden-product-type.enum.ts deleted file mode 100644 index 4389d283c00..00000000000 --- a/libs/common/src/billing/enums/bitwarden-product-type.enum.ts +++ /dev/null @@ -1,6 +0,0 @@ -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum BitwardenProductType { - PasswordManager = 0, - SecretsManager = 1, -} diff --git a/libs/common/src/billing/enums/index.ts b/libs/common/src/billing/enums/index.ts index 1a9f3f8219c..ee8cd1f5948 100644 --- a/libs/common/src/billing/enums/index.ts +++ b/libs/common/src/billing/enums/index.ts @@ -2,7 +2,6 @@ export * from "./payment-method-type.enum"; export * from "./plan-sponsorship-type.enum"; export * from "./plan-type.enum"; export * from "./transaction-type.enum"; -export * from "./bitwarden-product-type.enum"; export * from "./product-tier-type.enum"; export * from "./product-type.enum"; export * from "./plan-interval.enum"; diff --git a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts deleted file mode 100644 index 83b254ac512..00000000000 --- a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts +++ /dev/null @@ -1,29 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { TaxInformation } from "../domain/tax-information"; - -import { TaxInfoUpdateRequest } from "./tax-info-update.request"; - -export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest { - taxId: string; - line1: string; - line2: string; - city: string; - state: string; - - static From(taxInformation: TaxInformation): ExpandedTaxInfoUpdateRequest { - if (!taxInformation) { - return null; - } - - const request = new ExpandedTaxInfoUpdateRequest(); - request.country = taxInformation.country; - request.postalCode = taxInformation.postalCode; - request.taxId = taxInformation.taxId; - request.line1 = taxInformation.line1; - request.line2 = taxInformation.line2; - request.city = taxInformation.city; - request.state = taxInformation.state; - return request; - } -} diff --git a/libs/common/src/billing/models/request/payment.request.ts b/libs/common/src/billing/models/request/payment.request.ts deleted file mode 100644 index e2edd9aabb3..00000000000 --- a/libs/common/src/billing/models/request/payment.request.ts +++ /dev/null @@ -1,10 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { PaymentMethodType } from "../../enums"; - -import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request"; - -export class PaymentRequest extends ExpandedTaxInfoUpdateRequest { - paymentMethodType: PaymentMethodType; - paymentToken: string; -} diff --git a/libs/common/src/billing/models/request/preview-individual-invoice.request.ts b/libs/common/src/billing/models/request/preview-individual-invoice.request.ts deleted file mode 100644 index f817398c629..00000000000 --- a/libs/common/src/billing/models/request/preview-individual-invoice.request.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-strict-ignore -export class PreviewIndividualInvoiceRequest { - passwordManager: PasswordManager; - taxInformation: TaxInformation; - - constructor(passwordManager: PasswordManager, taxInformation: TaxInformation) { - this.passwordManager = passwordManager; - this.taxInformation = taxInformation; - } -} - -class PasswordManager { - additionalStorage: number; - - constructor(additionalStorage: number) { - this.additionalStorage = additionalStorage; - } -} - -class TaxInformation { - postalCode: string; - country: string; - - constructor(postalCode: string, country: string) { - this.postalCode = postalCode; - this.country = country; - } -} diff --git a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts deleted file mode 100644 index bfeecb4eb23..00000000000 --- a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { PlanSponsorshipType, PlanType } from "../../enums"; - -export class PreviewOrganizationInvoiceRequest { - organizationId?: string; - passwordManager: PasswordManager; - secretsManager?: SecretsManager; - taxInformation: TaxInformation; - - constructor( - passwordManager: PasswordManager, - taxInformation: TaxInformation, - organizationId?: string, - secretsManager?: SecretsManager, - ) { - this.organizationId = organizationId; - this.passwordManager = passwordManager; - this.secretsManager = secretsManager; - this.taxInformation = taxInformation; - } -} - -class PasswordManager { - plan: PlanType; - sponsoredPlan?: PlanSponsorshipType; - seats: number; - additionalStorage: number; - - constructor(plan: PlanType, seats: number, additionalStorage: number) { - this.plan = plan; - this.seats = seats; - this.additionalStorage = additionalStorage; - } -} - -class SecretsManager { - seats: number; - additionalMachineAccounts: number; - - constructor(seats: number, additionalMachineAccounts: number) { - this.seats = seats; - this.additionalMachineAccounts = additionalMachineAccounts; - } -} - -class TaxInformation { - postalCode: string; - country: string; - taxId: string; - - constructor(postalCode: string, country: string, taxId: string) { - this.postalCode = postalCode; - this.country = country; - this.taxId = taxId; - } -} diff --git a/libs/common/src/billing/models/request/tax-info-update.request.ts b/libs/common/src/billing/models/request/tax-info-update.request.ts deleted file mode 100644 index 6f767535472..00000000000 --- a/libs/common/src/billing/models/request/tax-info-update.request.ts +++ /dev/null @@ -1,6 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -export class TaxInfoUpdateRequest { - country: string; - postalCode: string; -} diff --git a/libs/common/src/billing/models/request/tax/index.ts b/libs/common/src/billing/models/request/tax/index.ts deleted file mode 100644 index cda1930c614..00000000000 --- a/libs/common/src/billing/models/request/tax/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./preview-tax-amount-for-organization-trial.request"; diff --git a/libs/common/src/billing/models/request/tax/preview-tax-amount-for-organization-trial.request.ts b/libs/common/src/billing/models/request/tax/preview-tax-amount-for-organization-trial.request.ts deleted file mode 100644 index 3f366335a47..00000000000 --- a/libs/common/src/billing/models/request/tax/preview-tax-amount-for-organization-trial.request.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PlanType, ProductType } from "../../../enums"; - -export type PreviewTaxAmountForOrganizationTrialRequest = { - planType: PlanType; - productType: ProductType; - taxInformation: { - country: string; - postalCode: string; - taxId?: string; - }; -}; diff --git a/libs/common/src/billing/models/request/tokenized-payment-source.request.ts b/libs/common/src/billing/models/request/tokenized-payment-source.request.ts deleted file mode 100644 index e4bf575cc6a..00000000000 --- a/libs/common/src/billing/models/request/tokenized-payment-source.request.ts +++ /dev/null @@ -1,8 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { PaymentMethodType } from "../../enums"; - -export class TokenizedPaymentSourceRequest { - type: PaymentMethodType; - token: string; -} diff --git a/libs/common/src/billing/models/request/update-payment-method.request.ts b/libs/common/src/billing/models/request/update-payment-method.request.ts deleted file mode 100644 index 10b03103716..00000000000 --- a/libs/common/src/billing/models/request/update-payment-method.request.ts +++ /dev/null @@ -1,9 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request"; -import { TokenizedPaymentSourceRequest } from "./tokenized-payment-source.request"; - -export class UpdatePaymentMethodRequest { - paymentSource: TokenizedPaymentSourceRequest; - taxInformation: ExpandedTaxInfoUpdateRequest; -} diff --git a/libs/common/src/billing/models/request/verify-bank-account.request.ts b/libs/common/src/billing/models/request/verify-bank-account.request.ts deleted file mode 100644 index ee85d1a2aad..00000000000 --- a/libs/common/src/billing/models/request/verify-bank-account.request.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class VerifyBankAccountRequest { - descriptorCode: string; - - constructor(descriptorCode: string) { - this.descriptorCode = descriptorCode; - } -} diff --git a/libs/common/src/billing/models/response/billing-payment.response.ts b/libs/common/src/billing/models/response/billing-payment.response.ts deleted file mode 100644 index e60a11c0772..00000000000 --- a/libs/common/src/billing/models/response/billing-payment.response.ts +++ /dev/null @@ -1,17 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { BaseResponse } from "../../../models/response/base.response"; - -import { BillingSourceResponse } from "./billing.response"; - -export class BillingPaymentResponse extends BaseResponse { - balance: number; - paymentSource: BillingSourceResponse; - - constructor(response: any) { - super(response); - this.balance = this.getResponseProperty("Balance"); - const paymentSource = this.getResponseProperty("PaymentSource"); - this.paymentSource = paymentSource == null ? null : new BillingSourceResponse(paymentSource); - } -} diff --git a/libs/common/src/billing/models/response/payment-method.response.ts b/libs/common/src/billing/models/response/payment-method.response.ts deleted file mode 100644 index 34e95032aef..00000000000 --- a/libs/common/src/billing/models/response/payment-method.response.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BaseResponse } from "../../../models/response/base.response"; - -import { PaymentSourceResponse } from "./payment-source.response"; -import { TaxInfoResponse } from "./tax-info.response"; - -export class PaymentMethodResponse extends BaseResponse { - accountCredit: number; - paymentSource?: PaymentSourceResponse; - subscriptionStatus?: string; - taxInformation?: TaxInfoResponse; - - constructor(response: any) { - super(response); - this.accountCredit = this.getResponseProperty("AccountCredit"); - - const paymentSource = this.getResponseProperty("PaymentSource"); - if (paymentSource) { - this.paymentSource = new PaymentSourceResponse(paymentSource); - } - - this.subscriptionStatus = this.getResponseProperty("SubscriptionStatus"); - - const taxInformation = this.getResponseProperty("TaxInformation"); - if (taxInformation) { - this.taxInformation = new TaxInfoResponse(taxInformation); - } - } -} diff --git a/libs/common/src/billing/models/response/tax-id-types.response.ts b/libs/common/src/billing/models/response/tax-id-types.response.ts deleted file mode 100644 index f31f2133b34..00000000000 --- a/libs/common/src/billing/models/response/tax-id-types.response.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BaseResponse } from "../../../models/response/base.response"; - -export class TaxIdTypesResponse extends BaseResponse { - taxIdTypes: TaxIdTypeResponse[] = []; - - constructor(response: any) { - super(response); - const taxIdTypes = this.getResponseProperty("TaxIdTypes"); - if (taxIdTypes && taxIdTypes.length) { - this.taxIdTypes = taxIdTypes.map((t: any) => new TaxIdTypeResponse(t)); - } - } -} - -export class TaxIdTypeResponse extends BaseResponse { - code: string; - country: string; - description: string; - example: string; - - constructor(response: any) { - super(response); - this.code = this.getResponseProperty("Code"); - this.country = this.getResponseProperty("Country"); - this.description = this.getResponseProperty("Description"); - this.example = this.getResponseProperty("Example"); - } -} diff --git a/libs/common/src/billing/models/response/tax/index.ts b/libs/common/src/billing/models/response/tax/index.ts deleted file mode 100644 index 525d6d7c80a..00000000000 --- a/libs/common/src/billing/models/response/tax/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./preview-tax-amount.response"; diff --git a/libs/common/src/billing/models/response/tax/preview-tax-amount.response.ts b/libs/common/src/billing/models/response/tax/preview-tax-amount.response.ts deleted file mode 100644 index cf15156551a..00000000000 --- a/libs/common/src/billing/models/response/tax/preview-tax-amount.response.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BaseResponse } from "../../../../models/response/base.response"; - -export class PreviewTaxAmountResponse extends BaseResponse { - taxAmount: number; - - constructor(response: any) { - super(response); - - this.taxAmount = this.getResponseProperty("TaxAmount"); - } -} diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 2292f26e616..a34809e9f02 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,23 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; - import { ApiService } from "../../abstractions/api.service"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; import { ListResponse } from "../../models/response/list.response"; import { BillingApiServiceAbstraction } from "../abstractions"; -import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; -import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request"; import { SubscriptionCancellationRequest } from "../models/request/subscription-cancellation.request"; import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; -import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request"; -import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request"; import { InvoicesResponse } from "../models/response/invoices.response"; import { OrganizationBillingMetadataResponse } from "../models/response/organization-billing-metadata.response"; -import { PaymentMethodResponse } from "../models/response/payment-method.response"; import { PlanResponse } from "../models/response/plan.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; @@ -54,21 +47,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { ); } - async createSetupIntent(type: PaymentMethodType) { - const getPath = () => { - switch (type) { - case PaymentMethodType.BankAccount: { - return "/setup-intent/bank-account"; - } - case PaymentMethodType.Card: { - return "/setup-intent/card"; - } - } - }; - const response = await this.apiService.send("POST", getPath(), null, true, true); - return response as string; - } - async getOrganizationBillingMetadata( organizationId: string, ): Promise { @@ -83,17 +61,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new OrganizationBillingMetadataResponse(r); } - async getOrganizationPaymentMethod(organizationId: string): Promise { - const response = await this.apiService.send( - "GET", - "/organizations/" + organizationId + "/billing/payment-method", - null, - true, - true, - ); - return new PaymentMethodResponse(response); - } - async getPlans(): Promise> { const r = await this.apiService.send("GET", "/plans", null, false, true); return new ListResponse(r, PlanResponse); @@ -145,43 +112,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new ProviderSubscriptionResponse(response); } - async getProviderTaxInformation(providerId: string): Promise { - const response = await this.apiService.send( - "GET", - "/providers/" + providerId + "/billing/tax-information", - null, - true, - true, - ); - return new TaxInfoResponse(response); - } - - async updateOrganizationPaymentMethod( - organizationId: string, - request: UpdatePaymentMethodRequest, - ): Promise { - return await this.apiService.send( - "PUT", - "/organizations/" + organizationId + "/billing/payment-method", - request, - true, - false, - ); - } - - async updateOrganizationTaxInformation( - organizationId: string, - request: ExpandedTaxInfoUpdateRequest, - ): Promise { - return await this.apiService.send( - "PUT", - "/organizations/" + organizationId + "/billing/tax-information", - request, - true, - false, - ); - } - async updateProviderClientOrganization( providerId: string, organizationId: string, @@ -196,55 +126,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { ); } - async updateProviderPaymentMethod( - providerId: string, - request: UpdatePaymentMethodRequest, - ): Promise { - return await this.apiService.send( - "PUT", - "/providers/" + providerId + "/billing/payment-method", - request, - true, - false, - ); - } - - async updateProviderTaxInformation(providerId: string, request: ExpandedTaxInfoUpdateRequest) { - return await this.apiService.send( - "PUT", - "/providers/" + providerId + "/billing/tax-information", - request, - true, - false, - ); - } - - async verifyOrganizationBankAccount( - organizationId: string, - request: VerifyBankAccountRequest, - ): Promise { - return await this.apiService.send( - "POST", - "/organizations/" + organizationId + "/billing/payment-method/verify-bank-account", - request, - true, - false, - ); - } - - async verifyProviderBankAccount( - providerId: string, - request: VerifyBankAccountRequest, - ): Promise { - return await this.apiService.send( - "POST", - "/providers/" + providerId + "/billing/payment-method/verify-bank-account", - request, - true, - false, - ); - } - async restartSubscription( organizationId: string, request: OrganizationCreateRequest, diff --git a/libs/common/src/billing/services/organization-billing.service.spec.ts b/libs/common/src/billing/services/organization-billing.service.spec.ts index 42cfb4a5371..a14dd0f0279 100644 --- a/libs/common/src/billing/services/organization-billing.service.spec.ts +++ b/libs/common/src/billing/services/organization-billing.service.spec.ts @@ -23,7 +23,6 @@ import { OrganizationResponse } from "../../admin-console/models/response/organi import { EncString } from "../../key-management/crypto/models/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { OrgKey } from "../../types/key"; -import { PaymentMethodResponse } from "../models/response/payment-method.response"; describe("OrganizationBillingService", () => { let apiService: jest.Mocked; @@ -62,47 +61,6 @@ describe("OrganizationBillingService", () => { return jest.resetAllMocks(); }); - describe("getPaymentSource()", () => { - it("given a valid organization id, then it returns a payment source", async () => { - //Arrange - const orgId = "organization-test"; - const paymentMethodResponse = { - paymentSource: { type: PaymentMethodType.Card }, - } as PaymentMethodResponse; - billingApiService.getOrganizationPaymentMethod.mockResolvedValue(paymentMethodResponse); - - //Act - const returnedPaymentSource = await sut.getPaymentSource(orgId); - - //Assert - expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1); - expect(returnedPaymentSource).toEqual(paymentMethodResponse.paymentSource); - }); - - it("given an invalid organizationId, it should return undefined", async () => { - //Arrange - const orgId = "invalid-id"; - billingApiService.getOrganizationPaymentMethod.mockResolvedValue(null); - - //Act - const returnedPaymentSource = await sut.getPaymentSource(orgId); - - //Assert - expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1); - expect(returnedPaymentSource).toBeUndefined(); - }); - - it("given an API error occurs, then it throws the error", async () => { - // Arrange - const orgId = "error-org"; - billingApiService.getOrganizationPaymentMethod.mockRejectedValue(new Error("API Error")); - - // Act & Assert - await expect(sut.getPaymentSource(orgId)).rejects.toThrow("API Error"); - expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1); - }); - }); - describe("purchaseSubscription()", () => { it("given valid subscription information, then it returns successful response", async () => { //Arrange @@ -118,7 +76,7 @@ describe("OrganizationBillingService", () => { const organizationResponse = { name: subscriptionInformation.organization.name, billingEmail: subscriptionInformation.organization.billingEmail, - planType: subscriptionInformation.plan.type, + planType: subscriptionInformation.plan!.type, } as OrganizationResponse; organizationApiService.create.mockResolvedValue(organizationResponse); @@ -201,8 +159,8 @@ describe("OrganizationBillingService", () => { const organizationResponse = { name: subscriptionInformation.organization.name, - plan: { type: subscriptionInformation.plan.type }, - planType: subscriptionInformation.plan.type, + plan: { type: subscriptionInformation.plan!.type }, + planType: subscriptionInformation.plan!.type, } as OrganizationResponse; organizationApiService.createWithoutPayment.mockResolvedValue(organizationResponse); @@ -262,7 +220,7 @@ describe("OrganizationBillingService", () => { const organizationResponse = { name: subscriptionInformation.organization.name, billingEmail: subscriptionInformation.organization.billingEmail, - planType: subscriptionInformation.plan.type, + planType: subscriptionInformation.plan!.type, } as OrganizationResponse; organizationApiService.create.mockResolvedValue(organizationResponse); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index 53ce727df68..4120047a15f 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -25,7 +25,6 @@ import { } from "../abstractions"; import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; -import { PaymentSourceResponse } from "../models/response/payment-source.response"; interface OrganizationKeys { encryptedKey: EncString; @@ -45,11 +44,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} - async getPaymentSource(organizationId: string): Promise { - const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId); - return paymentMethod?.paymentSource; - } - async purchaseSubscription( subscription: SubscriptionInformation, activeUserId: UserId, diff --git a/libs/common/src/billing/services/tax.service.ts b/libs/common/src/billing/services/tax.service.ts deleted file mode 100644 index 27966016913..00000000000 --- a/libs/common/src/billing/services/tax.service.ts +++ /dev/null @@ -1,318 +0,0 @@ -import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax"; - -import { ApiService } from "../../abstractions/api.service"; -import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction"; -import { CountryListItem } from "../models/domain"; -import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; -import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; -import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; - -export class TaxService implements TaxServiceAbstraction { - constructor(private apiService: ApiService) {} - - getCountries(): CountryListItem[] { - return [ - { name: "-- Select --", value: "", disabled: false }, - { name: "United States", value: "US", disabled: false }, - { name: "China", value: "CN", disabled: false }, - { name: "France", value: "FR", disabled: false }, - { name: "Germany", value: "DE", disabled: false }, - { name: "Canada", value: "CA", disabled: false }, - { name: "United Kingdom", value: "GB", disabled: false }, - { name: "Australia", value: "AU", disabled: false }, - { name: "India", value: "IN", disabled: false }, - { name: "", value: "-", disabled: true }, - { name: "Afghanistan", value: "AF", disabled: false }, - { name: "Åland Islands", value: "AX", disabled: false }, - { name: "Albania", value: "AL", disabled: false }, - { name: "Algeria", value: "DZ", disabled: false }, - { name: "American Samoa", value: "AS", disabled: false }, - { name: "Andorra", value: "AD", disabled: false }, - { name: "Angola", value: "AO", disabled: false }, - { name: "Anguilla", value: "AI", disabled: false }, - { name: "Antarctica", value: "AQ", disabled: false }, - { name: "Antigua and Barbuda", value: "AG", disabled: false }, - { name: "Argentina", value: "AR", disabled: false }, - { name: "Armenia", value: "AM", disabled: false }, - { name: "Aruba", value: "AW", disabled: false }, - { name: "Austria", value: "AT", disabled: false }, - { name: "Azerbaijan", value: "AZ", disabled: false }, - { name: "Bahamas", value: "BS", disabled: false }, - { name: "Bahrain", value: "BH", disabled: false }, - { name: "Bangladesh", value: "BD", disabled: false }, - { name: "Barbados", value: "BB", disabled: false }, - { name: "Belarus", value: "BY", disabled: false }, - { name: "Belgium", value: "BE", disabled: false }, - { name: "Belize", value: "BZ", disabled: false }, - { name: "Benin", value: "BJ", disabled: false }, - { name: "Bermuda", value: "BM", disabled: false }, - { name: "Bhutan", value: "BT", disabled: false }, - { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, - { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, - { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, - { name: "Botswana", value: "BW", disabled: false }, - { name: "Bouvet Island", value: "BV", disabled: false }, - { name: "Brazil", value: "BR", disabled: false }, - { name: "British Indian Ocean Territory", value: "IO", disabled: false }, - { name: "Brunei Darussalam", value: "BN", disabled: false }, - { name: "Bulgaria", value: "BG", disabled: false }, - { name: "Burkina Faso", value: "BF", disabled: false }, - { name: "Burundi", value: "BI", disabled: false }, - { name: "Cambodia", value: "KH", disabled: false }, - { name: "Cameroon", value: "CM", disabled: false }, - { name: "Cape Verde", value: "CV", disabled: false }, - { name: "Cayman Islands", value: "KY", disabled: false }, - { name: "Central African Republic", value: "CF", disabled: false }, - { name: "Chad", value: "TD", disabled: false }, - { name: "Chile", value: "CL", disabled: false }, - { name: "Christmas Island", value: "CX", disabled: false }, - { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, - { name: "Colombia", value: "CO", disabled: false }, - { name: "Comoros", value: "KM", disabled: false }, - { name: "Congo", value: "CG", disabled: false }, - { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, - { name: "Cook Islands", value: "CK", disabled: false }, - { name: "Costa Rica", value: "CR", disabled: false }, - { name: "Côte d'Ivoire", value: "CI", disabled: false }, - { name: "Croatia", value: "HR", disabled: false }, - { name: "Cuba", value: "CU", disabled: false }, - { name: "Curaçao", value: "CW", disabled: false }, - { name: "Cyprus", value: "CY", disabled: false }, - { name: "Czech Republic", value: "CZ", disabled: false }, - { name: "Denmark", value: "DK", disabled: false }, - { name: "Djibouti", value: "DJ", disabled: false }, - { name: "Dominica", value: "DM", disabled: false }, - { name: "Dominican Republic", value: "DO", disabled: false }, - { name: "Ecuador", value: "EC", disabled: false }, - { name: "Egypt", value: "EG", disabled: false }, - { name: "El Salvador", value: "SV", disabled: false }, - { name: "Equatorial Guinea", value: "GQ", disabled: false }, - { name: "Eritrea", value: "ER", disabled: false }, - { name: "Estonia", value: "EE", disabled: false }, - { name: "Ethiopia", value: "ET", disabled: false }, - { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, - { name: "Faroe Islands", value: "FO", disabled: false }, - { name: "Fiji", value: "FJ", disabled: false }, - { name: "Finland", value: "FI", disabled: false }, - { name: "French Guiana", value: "GF", disabled: false }, - { name: "French Polynesia", value: "PF", disabled: false }, - { name: "French Southern Territories", value: "TF", disabled: false }, - { name: "Gabon", value: "GA", disabled: false }, - { name: "Gambia", value: "GM", disabled: false }, - { name: "Georgia", value: "GE", disabled: false }, - { name: "Ghana", value: "GH", disabled: false }, - { name: "Gibraltar", value: "GI", disabled: false }, - { name: "Greece", value: "GR", disabled: false }, - { name: "Greenland", value: "GL", disabled: false }, - { name: "Grenada", value: "GD", disabled: false }, - { name: "Guadeloupe", value: "GP", disabled: false }, - { name: "Guam", value: "GU", disabled: false }, - { name: "Guatemala", value: "GT", disabled: false }, - { name: "Guernsey", value: "GG", disabled: false }, - { name: "Guinea", value: "GN", disabled: false }, - { name: "Guinea-Bissau", value: "GW", disabled: false }, - { name: "Guyana", value: "GY", disabled: false }, - { name: "Haiti", value: "HT", disabled: false }, - { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, - { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, - { name: "Honduras", value: "HN", disabled: false }, - { name: "Hong Kong", value: "HK", disabled: false }, - { name: "Hungary", value: "HU", disabled: false }, - { name: "Iceland", value: "IS", disabled: false }, - { name: "Indonesia", value: "ID", disabled: false }, - { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, - { name: "Iraq", value: "IQ", disabled: false }, - { name: "Ireland", value: "IE", disabled: false }, - { name: "Isle of Man", value: "IM", disabled: false }, - { name: "Israel", value: "IL", disabled: false }, - { name: "Italy", value: "IT", disabled: false }, - { name: "Jamaica", value: "JM", disabled: false }, - { name: "Japan", value: "JP", disabled: false }, - { name: "Jersey", value: "JE", disabled: false }, - { name: "Jordan", value: "JO", disabled: false }, - { name: "Kazakhstan", value: "KZ", disabled: false }, - { name: "Kenya", value: "KE", disabled: false }, - { name: "Kiribati", value: "KI", disabled: false }, - { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, - { name: "Korea, Republic of", value: "KR", disabled: false }, - { name: "Kuwait", value: "KW", disabled: false }, - { name: "Kyrgyzstan", value: "KG", disabled: false }, - { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, - { name: "Latvia", value: "LV", disabled: false }, - { name: "Lebanon", value: "LB", disabled: false }, - { name: "Lesotho", value: "LS", disabled: false }, - { name: "Liberia", value: "LR", disabled: false }, - { name: "Libya", value: "LY", disabled: false }, - { name: "Liechtenstein", value: "LI", disabled: false }, - { name: "Lithuania", value: "LT", disabled: false }, - { name: "Luxembourg", value: "LU", disabled: false }, - { name: "Macao", value: "MO", disabled: false }, - { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, - { name: "Madagascar", value: "MG", disabled: false }, - { name: "Malawi", value: "MW", disabled: false }, - { name: "Malaysia", value: "MY", disabled: false }, - { name: "Maldives", value: "MV", disabled: false }, - { name: "Mali", value: "ML", disabled: false }, - { name: "Malta", value: "MT", disabled: false }, - { name: "Marshall Islands", value: "MH", disabled: false }, - { name: "Martinique", value: "MQ", disabled: false }, - { name: "Mauritania", value: "MR", disabled: false }, - { name: "Mauritius", value: "MU", disabled: false }, - { name: "Mayotte", value: "YT", disabled: false }, - { name: "Mexico", value: "MX", disabled: false }, - { name: "Micronesia, Federated States of", value: "FM", disabled: false }, - { name: "Moldova, Republic of", value: "MD", disabled: false }, - { name: "Monaco", value: "MC", disabled: false }, - { name: "Mongolia", value: "MN", disabled: false }, - { name: "Montenegro", value: "ME", disabled: false }, - { name: "Montserrat", value: "MS", disabled: false }, - { name: "Morocco", value: "MA", disabled: false }, - { name: "Mozambique", value: "MZ", disabled: false }, - { name: "Myanmar", value: "MM", disabled: false }, - { name: "Namibia", value: "NA", disabled: false }, - { name: "Nauru", value: "NR", disabled: false }, - { name: "Nepal", value: "NP", disabled: false }, - { name: "Netherlands", value: "NL", disabled: false }, - { name: "New Caledonia", value: "NC", disabled: false }, - { name: "New Zealand", value: "NZ", disabled: false }, - { name: "Nicaragua", value: "NI", disabled: false }, - { name: "Niger", value: "NE", disabled: false }, - { name: "Nigeria", value: "NG", disabled: false }, - { name: "Niue", value: "NU", disabled: false }, - { name: "Norfolk Island", value: "NF", disabled: false }, - { name: "Northern Mariana Islands", value: "MP", disabled: false }, - { name: "Norway", value: "NO", disabled: false }, - { name: "Oman", value: "OM", disabled: false }, - { name: "Pakistan", value: "PK", disabled: false }, - { name: "Palau", value: "PW", disabled: false }, - { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, - { name: "Panama", value: "PA", disabled: false }, - { name: "Papua New Guinea", value: "PG", disabled: false }, - { name: "Paraguay", value: "PY", disabled: false }, - { name: "Peru", value: "PE", disabled: false }, - { name: "Philippines", value: "PH", disabled: false }, - { name: "Pitcairn", value: "PN", disabled: false }, - { name: "Poland", value: "PL", disabled: false }, - { name: "Portugal", value: "PT", disabled: false }, - { name: "Puerto Rico", value: "PR", disabled: false }, - { name: "Qatar", value: "QA", disabled: false }, - { name: "Réunion", value: "RE", disabled: false }, - { name: "Romania", value: "RO", disabled: false }, - { name: "Russian Federation", value: "RU", disabled: false }, - { name: "Rwanda", value: "RW", disabled: false }, - { name: "Saint Barthélemy", value: "BL", disabled: false }, - { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, - { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, - { name: "Saint Lucia", value: "LC", disabled: false }, - { name: "Saint Martin (French part)", value: "MF", disabled: false }, - { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, - { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, - { name: "Samoa", value: "WS", disabled: false }, - { name: "San Marino", value: "SM", disabled: false }, - { name: "Sao Tome and Principe", value: "ST", disabled: false }, - { name: "Saudi Arabia", value: "SA", disabled: false }, - { name: "Senegal", value: "SN", disabled: false }, - { name: "Serbia", value: "RS", disabled: false }, - { name: "Seychelles", value: "SC", disabled: false }, - { name: "Sierra Leone", value: "SL", disabled: false }, - { name: "Singapore", value: "SG", disabled: false }, - { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, - { name: "Slovakia", value: "SK", disabled: false }, - { name: "Slovenia", value: "SI", disabled: false }, - { name: "Solomon Islands", value: "SB", disabled: false }, - { name: "Somalia", value: "SO", disabled: false }, - { name: "South Africa", value: "ZA", disabled: false }, - { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, - { name: "South Sudan", value: "SS", disabled: false }, - { name: "Spain", value: "ES", disabled: false }, - { name: "Sri Lanka", value: "LK", disabled: false }, - { name: "Sudan", value: "SD", disabled: false }, - { name: "Suriname", value: "SR", disabled: false }, - { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, - { name: "Swaziland", value: "SZ", disabled: false }, - { name: "Sweden", value: "SE", disabled: false }, - { name: "Switzerland", value: "CH", disabled: false }, - { name: "Syrian Arab Republic", value: "SY", disabled: false }, - { name: "Taiwan", value: "TW", disabled: false }, - { name: "Tajikistan", value: "TJ", disabled: false }, - { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, - { name: "Thailand", value: "TH", disabled: false }, - { name: "Timor-Leste", value: "TL", disabled: false }, - { name: "Togo", value: "TG", disabled: false }, - { name: "Tokelau", value: "TK", disabled: false }, - { name: "Tonga", value: "TO", disabled: false }, - { name: "Trinidad and Tobago", value: "TT", disabled: false }, - { name: "Tunisia", value: "TN", disabled: false }, - { name: "Turkey", value: "TR", disabled: false }, - { name: "Turkmenistan", value: "TM", disabled: false }, - { name: "Turks and Caicos Islands", value: "TC", disabled: false }, - { name: "Tuvalu", value: "TV", disabled: false }, - { name: "Uganda", value: "UG", disabled: false }, - { name: "Ukraine", value: "UA", disabled: false }, - { name: "United Arab Emirates", value: "AE", disabled: false }, - { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, - { name: "Uruguay", value: "UY", disabled: false }, - { name: "Uzbekistan", value: "UZ", disabled: false }, - { name: "Vanuatu", value: "VU", disabled: false }, - { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, - { name: "Viet Nam", value: "VN", disabled: false }, - { name: "Virgin Islands, British", value: "VG", disabled: false }, - { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, - { name: "Wallis and Futuna", value: "WF", disabled: false }, - { name: "Western Sahara", value: "EH", disabled: false }, - { name: "Yemen", value: "YE", disabled: false }, - { name: "Zambia", value: "ZM", disabled: false }, - { name: "Zimbabwe", value: "ZW", disabled: false }, - ]; - } - - async isCountrySupported(country: string): Promise { - const response = await this.apiService.send( - "GET", - "/tax/is-country-supported?country=" + country, - null, - true, - true, - ); - return response; - } - - async previewIndividualInvoice( - request: PreviewIndividualInvoiceRequest, - ): Promise { - const response = await this.apiService.send( - "POST", - "/accounts/billing/preview-invoice", - request, - true, - true, - ); - return new PreviewInvoiceResponse(response); - } - - async previewOrganizationInvoice( - request: PreviewOrganizationInvoiceRequest, - ): Promise { - const response = await this.apiService.send( - "POST", - `/invoices/preview-organization`, - request, - true, - true, - ); - return new PreviewInvoiceResponse(response); - } - - async previewTaxAmountForOrganizationTrial( - request: PreviewTaxAmountForOrganizationTrialRequest, - ): Promise { - const response = await this.apiService.send( - "POST", - "/tax/preview-amount/organization-trial", - request, - true, - true, - ); - return response as number; - } -} diff --git a/libs/common/src/enums/event-type.enum.ts b/libs/common/src/enums/event-type.enum.ts index f6a48fe23bb..b3b12118ede 100644 --- a/libs/common/src/enums/event-type.enum.ts +++ b/libs/common/src/enums/event-type.enum.ts @@ -108,4 +108,11 @@ export enum EventType { Project_Created = 2201, Project_Edited = 2202, Project_Deleted = 2203, + + ServiceAccount_UserAdded = 2300, + ServiceAccount_UserRemoved = 2301, + ServiceAccount_GroupAdded = 2302, + ServiceAccount_GroupRemoved = 2303, + ServiceAccount_Created = 2304, + ServiceAccount_Deleted = 2305, } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index bd874f934f0..578d09c9aea 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -12,7 +12,6 @@ import { ServerConfig } from "../platform/abstractions/config/server-config"; export enum FeatureFlag { /* Admin Console Team */ CreateDefaultLocation = "pm-19467-create-default-location", - CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors", /* Auth */ PM22110_DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods", @@ -24,7 +23,6 @@ export enum FeatureFlag { /* Billing */ TrialPaymentOptional = "PM-8163-trial-payment", PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", - PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout", PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover", PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings", @@ -74,7 +72,6 @@ const FALSE = false as boolean; export const DefaultFeatureFlagValue = { /* Admin Console Team */ [FeatureFlag.CreateDefaultLocation]: FALSE, - [FeatureFlag.CollectionVaultRefactor]: FALSE, /* Autofill */ [FeatureFlag.MacOsNativeCredentialSync]: FALSE, @@ -102,7 +99,6 @@ export const DefaultFeatureFlagValue = { /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, - [FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE, [FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE, [FeatureFlag.PM22415_TaxIDWarnings]: FALSE, diff --git a/libs/common/src/models/response/event.response.ts b/libs/common/src/models/response/event.response.ts index 07124b3080c..41376e4086f 100644 --- a/libs/common/src/models/response/event.response.ts +++ b/libs/common/src/models/response/event.response.ts @@ -24,6 +24,7 @@ export class EventResponse extends BaseResponse { secretId: string; projectId: string; serviceAccountId: string; + grantedServiceAccountId: string; constructor(response: any) { super(response); @@ -48,5 +49,6 @@ export class EventResponse extends BaseResponse { this.secretId = this.getResponseProperty("SecretId"); this.projectId = this.getResponseProperty("ProjectId"); this.serviceAccountId = this.getResponseProperty("ServiceAccountId"); + this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId"); } } diff --git a/libs/common/src/platform/misc/safe-urls.ts b/libs/common/src/platform/misc/safe-urls.ts index d7223a344e4..f958f92aa19 100644 --- a/libs/common/src/platform/misc/safe-urls.ts +++ b/libs/common/src/platform/misc/safe-urls.ts @@ -17,13 +17,13 @@ const CanLaunchWhitelist = [ ]; export class SafeUrls { - static canLaunch(uri: string): boolean { + static canLaunch(uri: string | null | undefined): boolean { if (Utils.isNullOrWhitespace(uri)) { return false; } for (let i = 0; i < CanLaunchWhitelist.length; i++) { - if (uri.indexOf(CanLaunchWhitelist[i]) === 0) { + if (uri!.indexOf(CanLaunchWhitelist[i]) === 0) { return true; } } diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index 818138863fb..9f01db61fa6 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -302,7 +302,7 @@ describe("Utils Service", () => { expect(b64String).toBe(b64HelloWorldString); }); - runInBothEnvironments("should return an empty string for an empty ArrayBuffer", () => { + runInBothEnvironments("should return empty string for an empty ArrayBuffer", () => { const buffer = new Uint8Array([]).buffer; const b64String = Utils.fromBufferToB64(buffer); expect(b64String).toBe(""); @@ -312,6 +312,81 @@ describe("Utils Service", () => { const b64String = Utils.fromBufferToB64(null); expect(b64String).toBeNull(); }); + + runInBothEnvironments("returns null for undefined input", () => { + const b64 = Utils.fromBufferToB64(undefined as unknown as ArrayBuffer); + expect(b64).toBeNull(); + }); + + runInBothEnvironments("returns empty string for empty input", () => { + const b64 = Utils.fromBufferToB64(new ArrayBuffer(0)); + expect(b64).toBe(""); + }); + + runInBothEnvironments("accepts Uint8Array directly", () => { + const u8 = new Uint8Array(asciiHelloWorldArray); + const b64 = Utils.fromBufferToB64(u8); + expect(b64).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("respects byteOffset/byteLength (view window)", () => { + // [xx, 'hello world', yy] — view should only encode the middle slice + const prefix = [1, 2, 3]; + const suffix = [4, 5]; + const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]); + const view = new Uint8Array(all.buffer, prefix.length, asciiHelloWorldArray.length); + const b64 = Utils.fromBufferToB64(view); + expect(b64).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("handles DataView (ArrayBufferView other than Uint8Array)", () => { + const u8 = new Uint8Array(asciiHelloWorldArray); + const dv = new DataView(u8.buffer, 0, u8.byteLength); + const b64 = Utils.fromBufferToB64(dv); + expect(b64).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("handles DataView with offset/length window", () => { + // Buffer: [xx, 'hello world', yy] + const prefix = [9, 9, 9]; + const suffix = [8, 8]; + const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]); + + // DataView over just the "hello world" window + const dv = new DataView(all.buffer, prefix.length, asciiHelloWorldArray.length); + + const b64 = Utils.fromBufferToB64(dv); + expect(b64).toBe(b64HelloWorldString); + }); + + runInBothEnvironments( + "encodes empty view (offset-length window of zero) as empty string", + () => { + const backing = new Uint8Array([1, 2, 3, 4]); + const emptyView = new Uint8Array(backing.buffer, 2, 0); + const b64 = Utils.fromBufferToB64(emptyView); + expect(b64).toBe(""); + }, + ); + + runInBothEnvironments("does not mutate the input", () => { + const original = new Uint8Array(asciiHelloWorldArray); + const copyBefore = new Uint8Array(original); // snapshot + Utils.fromBufferToB64(original); + expect(original).toEqual(copyBefore); // unchanged + }); + + it("produces the same Base64 in Node vs non-Node mode", () => { + const bytes = new Uint8Array(asciiHelloWorldArray); + + Utils.isNode = true; + const nodeB64 = Utils.fromBufferToB64(bytes); + + Utils.isNode = false; + const browserB64 = Utils.fromBufferToB64(bytes); + + expect(browserB64).toBe(nodeB64); + }); }); describe("fromB64ToArray(...)", () => { diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index c103e346a85..5f977da3979 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -128,15 +128,52 @@ export class Utils { return arr; } - static fromBufferToB64(buffer: ArrayBuffer): string { + /** + * Convert binary data into a Base64 string. + * + * Overloads are provided for two categories of input: + * + * 1. ArrayBuffer + * - A raw, fixed-length chunk of memory (no element semantics). + * - Example: `const buf = new ArrayBuffer(16);` + * + * 2. ArrayBufferView + * - A *view* onto an existing buffer that gives the bytes meaning. + * - Examples: Uint8Array, Int32Array, DataView, etc. + * - Views can expose only a *window* of the underlying buffer via + * `byteOffset` and `byteLength`. + * Example: + * ```ts + * const buf = new ArrayBuffer(8); + * const full = new Uint8Array(buf); // sees all 8 bytes + * const half = new Uint8Array(buf, 4, 4); // sees only last 4 bytes + * ``` + * + * Returns: + * - Base64 string for non-empty inputs, + * - null if `buffer` is `null` or `undefined` + * - empty string if `buffer` is empty (0 bytes) + */ + static fromBufferToB64(buffer: null | undefined): null; + static fromBufferToB64(buffer: ArrayBuffer): string; + static fromBufferToB64(buffer: ArrayBufferView): string; + static fromBufferToB64(buffer: ArrayBuffer | ArrayBufferView | null | undefined): string | null { + // Handle null / undefined input if (buffer == null) { return null; } + + const bytes: Uint8Array = Utils.normalizeToUint8Array(buffer); + + // Handle empty input + if (bytes.length === 0) { + return ""; + } + if (Utils.isNode) { - return Buffer.from(buffer).toString("base64"); + return Buffer.from(bytes).toString("base64"); } else { let binary = ""; - const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } @@ -144,6 +181,30 @@ export class Utils { } } + /** + * Normalizes input into a Uint8Array so we always have a uniform, + * byte-level view of the data. This avoids dealing with differences + * between ArrayBuffer (raw memory with no indexing) and other typed + * views (which may have element sizes, offsets, and lengths). + * @param buffer ArrayBuffer or ArrayBufferView (e.g. Uint8Array, DataView, etc.) + */ + private static normalizeToUint8Array(buffer: ArrayBuffer | ArrayBufferView): Uint8Array { + /** + * 1) Uint8Array: already bytes → use directly. + * 2) ArrayBuffer: wrap whole buffer. + * 3) Other ArrayBufferView (e.g., DataView, Int32Array): + * wrap the view’s window (byteOffset..byteOffset+byteLength). + */ + if (buffer instanceof Uint8Array) { + return buffer; + } else if (buffer instanceof ArrayBuffer) { + return new Uint8Array(buffer); + } else { + const view = buffer as ArrayBufferView; + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); + } + } + static fromBufferToUrlB64(buffer: ArrayBuffer): string { return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer)); } @@ -314,7 +375,7 @@ export class Utils { } } - static getDomain(uriString: string): string { + static getDomain(uriString: string | null | undefined): string { if (Utils.isNullOrWhitespace(uriString)) { return null; } @@ -392,11 +453,11 @@ export class Utils { }; } - static isNullOrWhitespace(str: string): boolean { + static isNullOrWhitespace(str: string | null | undefined): boolean { return str == null || typeof str !== "string" || str.trim() === ""; } - static isNullOrEmpty(str: string | null): boolean { + static isNullOrEmpty(str: string | null | undefined): boolean { return str == null || typeof str !== "string" || str == ""; } @@ -418,7 +479,7 @@ export class Utils { return (Object.keys(obj).filter((k) => Number.isNaN(+k)) as K[]).map((k) => obj[k]); } - static getUrl(uriString: string): URL { + static getUrl(uriString: string | undefined | null): URL { if (this.isNullOrWhitespace(uriString)) { return null; } diff --git a/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts index 31f6ce10e01..a58b2d470e6 100644 --- a/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts +++ b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts @@ -27,8 +27,8 @@ export async function getCredentialsForAutofill( cipherId: cipher.id, credentialId: credId, rpId: credential.rpId, - userHandle: credential.userHandle, - userName: credential.userName, - }; + userHandle: credential.userHandle!, + userName: credential.userName!, + } satisfies Fido2CredentialAutofillView; }); } diff --git a/libs/common/src/platform/sync/default-sync.service.spec.ts b/libs/common/src/platform/sync/default-sync.service.spec.ts index 3c3e1c3677f..352e45b88b1 100644 --- a/libs/common/src/platform/sync/default-sync.service.spec.ts +++ b/libs/common/src/platform/sync/default-sync.service.spec.ts @@ -292,5 +292,100 @@ describe("DefaultSyncService", () => { expect(masterPasswordAbstraction.setMasterPasswordUnlockData).not.toHaveBeenCalled(); }); }); + + describe("mutate 'last update time'", () => { + let mockUserState: { update: jest.Mock }; + + const setupMockUserState = () => { + const mockUserState = { update: jest.fn() }; + jest.spyOn(stateProvider, "getUser").mockReturnValue(mockUserState as any); + return mockUserState; + }; + + const setupSyncScenario = (revisionDate: Date, lastSyncDate: Date) => { + jest.spyOn(apiService, "getAccountRevisionDate").mockResolvedValue(revisionDate.getTime()); + jest.spyOn(sut as any, "getLastSync").mockResolvedValue(lastSyncDate); + }; + + const expectUpdateCallCount = ( + mockUserState: { update: jest.Mock }, + expectedCount: number, + ) => { + if (expectedCount === 0) { + expect(mockUserState.update).not.toHaveBeenCalled(); + } else { + expect(mockUserState.update).toHaveBeenCalledTimes(expectedCount); + } + }; + + const defaultSyncOptions = { allowThrowOnError: true, skipTokenRefresh: true }; + const errorTolerantSyncOptions = { allowThrowOnError: false, skipTokenRefresh: true }; + + beforeEach(() => { + mockUserState = setupMockUserState(); + }); + + it("uses the current time when a sync is forced", async () => { + // Mock the value of this observable because it's used in `syncProfile`. Without it, the test breaks. + keyConnectorService.convertAccountRequired$ = of(false); + + // Baseline date/time to compare sync time to, in order to avoid needing to use some kind of fake date provider. + const beforeSync = Date.now(); + + // send it! + await sut.fullSync(true, defaultSyncOptions); + + expectUpdateCallCount(mockUserState, 1); + // Get the first and only call to update(...) + const updateCall = mockUserState.update.mock.calls[0]; + // Get the first argument to update(...) -- this will be the date callback that returns the date of the last successful sync + const dateCallback = updateCall[0]; + const actualTime = dateCallback() as Date; + + expect(Math.abs(actualTime.getTime() - beforeSync)).toBeLessThan(1); + }); + + it("updates last sync time when no sync is necessary", async () => { + const revisionDate = new Date(1); + setupSyncScenario(revisionDate, revisionDate); + + const syncResult = await sut.fullSync(false, defaultSyncOptions); + + // Sync should complete but return false since no sync was needed + expect(syncResult).toBe(false); + expectUpdateCallCount(mockUserState, 1); + }); + + it("updates last sync time when sync is successful", async () => { + setupSyncScenario(new Date(2), new Date(1)); + + const syncResult = await sut.fullSync(false, defaultSyncOptions); + + expect(syncResult).toBe(true); + expectUpdateCallCount(mockUserState, 1); + }); + + describe("error scenarios", () => { + it("does not update last sync time when sync fails", async () => { + apiService.getSync.mockRejectedValue(new Error("not connected")); + + const syncResult = await sut.fullSync(true, errorTolerantSyncOptions); + + expect(syncResult).toBe(false); + expectUpdateCallCount(mockUserState, 0); + }); + + it("does not update last sync time when account revision check fails", async () => { + jest + .spyOn(apiService, "getAccountRevisionDate") + .mockRejectedValue(new Error("not connected")); + + const syncResult = await sut.fullSync(false, errorTolerantSyncOptions); + + expect(syncResult).toBe(false); + expectUpdateCallCount(mockUserState, 0); + }); + }); + }); }); }); diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 582e4b58a64..a02d602dbf0 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -134,9 +134,11 @@ export class DefaultSyncService extends CoreSyncService { const now = new Date(); let needsSync = false; + let needsSyncSucceeded = true; try { needsSync = await this.needsSyncing(forceSync); } catch (e) { + needsSyncSucceeded = false; if (allowThrowOnError) { this.syncCompleted(false, userId); throw e; @@ -144,7 +146,9 @@ export class DefaultSyncService extends CoreSyncService { } if (!needsSync) { - await this.setLastSync(now, userId); + if (needsSyncSucceeded) { + await this.setLastSync(now, userId); + } return this.syncCompleted(false, userId); } diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 70ba76fe797..f000f35f126 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -90,14 +90,10 @@ import { } from "../auth/models/response/two-factor-web-authn.response"; import { TwoFactorYubiKeyResponse } from "../auth/models/response/two-factor-yubi-key.response"; import { BitPayInvoiceRequest } from "../billing/models/request/bit-pay-invoice.request"; -import { PaymentRequest } from "../billing/models/request/payment.request"; -import { TaxInfoUpdateRequest } from "../billing/models/request/tax-info-update.request"; import { BillingHistoryResponse } from "../billing/models/response/billing-history.response"; -import { BillingPaymentResponse } from "../billing/models/response/billing-payment.response"; import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; -import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; import { ClientType, DeviceType } from "../enums"; import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request"; import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request"; @@ -294,11 +290,6 @@ export class ApiService implements ApiServiceAbstraction { return new SubscriptionResponse(r); } - async getTaxInfo(): Promise { - const r = await this.send("GET", "/accounts/tax", null, true, true); - return new TaxInfoResponse(r); - } - async putProfile(request: UpdateProfileRequest): Promise { const r = await this.send("PUT", "/accounts/profile", request, true, true); return new ProfileResponse(r); @@ -309,10 +300,6 @@ export class ApiService implements ApiServiceAbstraction { return new ProfileResponse(r); } - putTaxInfo(request: TaxInfoUpdateRequest): Promise { - return this.send("PUT", "/accounts/tax", request, true, false); - } - async postPrelogin(request: PreloginRequest): Promise { const env = await firstValueFrom(this.environmentService.environment$); const r = await this.send( @@ -365,10 +352,6 @@ export class ApiService implements ApiServiceAbstraction { return new PaymentResponse(r); } - postAccountPayment(request: PaymentRequest): Promise { - return this.send("POST", "/accounts/payment", request, true, false); - } - postAccountLicense(data: FormData): Promise { return this.send("POST", "/accounts/license", data, true, false); } @@ -429,11 +412,6 @@ export class ApiService implements ApiServiceAbstraction { return new BillingHistoryResponse(r); } - async getUserBillingPayment(): Promise { - const r = await this.send("GET", "/accounts/billing/payment-method", null, true, true); - return new BillingPaymentResponse(r); - } - // Cipher APIs async getCipher(id: string): Promise { @@ -1294,6 +1272,28 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, EventResponse); } + async getEventsServiceAccount( + orgId: string, + id: string, + start: string, + end: string, + token: string, + ): Promise> { + const r = await this.send( + "GET", + this.addEventParameters( + "/organization/" + orgId + "/service-account/" + id + "/events", + start, + end, + token, + ), + null, + true, + true, + ); + return new ListResponse(r, EventResponse); + } + async getEventsProject( orgId: string, id: string, diff --git a/libs/vault/src/abstractions/cipher-archive.service.ts b/libs/common/src/vault/abstractions/cipher-archive.service.ts similarity index 81% rename from libs/vault/src/abstractions/cipher-archive.service.ts rename to libs/common/src/vault/abstractions/cipher-archive.service.ts index 6240e4001c8..cb6c38ddf67 100644 --- a/libs/vault/src/abstractions/cipher-archive.service.ts +++ b/libs/common/src/vault/abstractions/cipher-archive.service.ts @@ -1,7 +1,6 @@ import { Observable } from "rxjs"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; export abstract class CipherArchiveService { @@ -10,5 +9,4 @@ export abstract class CipherArchiveService { abstract showArchiveVault$(userId: UserId): Observable; abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise; abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise; - abstract canInteract(cipher: CipherView): Promise; } diff --git a/libs/common/src/vault/abstractions/search.service.ts b/libs/common/src/vault/abstractions/search.service.ts index 6b01302613c..233dee9ec75 100644 --- a/libs/common/src/vault/abstractions/search.service.ts +++ b/libs/common/src/vault/abstractions/search.service.ts @@ -30,6 +30,7 @@ export abstract class SearchService { ciphers: C[], query: string, deleted?: boolean, + archived?: boolean, ): C[]; abstract searchSends(sends: SendView[], query: string): SendView[]; } diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 5fff6b32aac..4ace8ce0e77 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -47,6 +47,7 @@ export class Attachment extends Domain { ): Promise { const view = await this.decryptObj( this, + // @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now. new AttachmentView(this), ["fileName"], orgId, diff --git a/libs/common/src/vault/models/domain/card.spec.ts b/libs/common/src/vault/models/domain/card.spec.ts index 5a134651e32..4da62c631d6 100644 --- a/libs/common/src/vault/models/domain/card.spec.ts +++ b/libs/common/src/vault/models/domain/card.spec.ts @@ -63,7 +63,6 @@ describe("Card", () => { expect(view).toEqual({ _brand: "brand", _number: "number", - _subTitle: null, cardholderName: "cardHolder", code: "code", expMonth: "expMonth", diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index b80f38f66af..8ba81c7bbd3 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -161,6 +161,8 @@ export class Cipher extends Domain implements Decryptable { await this.decryptObj( this, + // @ts-expect-error Ciphers have optional Ids which are getting swallowed by the ViewEncryptableKeys type + // The ViewEncryptableKeys type should be fixed to allow for optional Ids, but is out of scope for now. model, ["name", "notes"], this.organizationId, @@ -349,7 +351,7 @@ export class Cipher extends Domain implements Decryptable { */ toSdkCipher(): SdkCipher { const sdkCipher: SdkCipher = { - id: asUuid(this.id), + id: this.id ? asUuid(this.id) : undefined, organizationId: this.organizationId ? asUuid(this.organizationId) : undefined, folderId: this.folderId ? asUuid(this.folderId) : undefined, collectionIds: this.collectionIds ? this.collectionIds.map(asUuid) : ([] as any), diff --git a/libs/common/src/vault/models/domain/fido2-credential.ts b/libs/common/src/vault/models/domain/fido2-credential.ts index a74afc2336d..bdfac9a85ad 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.ts @@ -56,6 +56,7 @@ export class Fido2Credential extends Domain { async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { const view = await this.decryptObj( this, + // @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now. new Fido2CredentialView(), [ "credentialId", diff --git a/libs/common/src/vault/models/domain/field.ts b/libs/common/src/vault/models/domain/field.ts index f652a2820d4..130d1cc56d5 100644 --- a/libs/common/src/vault/models/domain/field.ts +++ b/libs/common/src/vault/models/domain/field.ts @@ -39,6 +39,7 @@ export class Field extends Domain { decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj( this, + // @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now. new FieldView(this), ["name", "value"], orgId, diff --git a/libs/common/src/vault/models/domain/identity.spec.ts b/libs/common/src/vault/models/domain/identity.spec.ts index c122c90371f..9fbcb92e4ae 100644 --- a/libs/common/src/vault/models/domain/identity.spec.ts +++ b/libs/common/src/vault/models/domain/identity.spec.ts @@ -112,7 +112,6 @@ describe("Identity", () => { expect(view).toEqual({ _firstName: "mockFirstName", _lastName: "mockLastName", - _subTitle: null, address1: "mockAddress1", address2: "mockAddress2", address3: "mockAddress3", 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 cbab41f1472..e67ba771412 100644 --- a/libs/common/src/vault/models/domain/login-uri.spec.ts +++ b/libs/common/src/vault/models/domain/login-uri.spec.ts @@ -56,10 +56,6 @@ describe("LoginUri", () => { const view = await loginUri.decrypt(null); expect(view).toEqual({ - _canLaunch: null, - _domain: null, - _host: null, - _hostname: null, _uri: "uri", match: 3, }); diff --git a/libs/common/src/vault/models/domain/login.spec.ts b/libs/common/src/vault/models/domain/login.spec.ts index dc3cc71fda8..99ceb2b0a3d 100644 --- a/libs/common/src/vault/models/domain/login.spec.ts +++ b/libs/common/src/vault/models/domain/login.spec.ts @@ -2,7 +2,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { mockEnc, mockFromJson } from "../../../../spec"; import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; -import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service"; +import { UriMatchStrategy } from "../../../models/domain/domain-service"; import { LoginData } from "../../models/data/login.data"; import { Login } from "../../models/domain/login"; import { LoginUri } from "../../models/domain/login-uri"; @@ -82,12 +82,7 @@ describe("Login DTO", () => { totp: "encrypted totp", uris: [ { - match: null as UriMatchStrategySetting, _uri: "decrypted uri", - _domain: null as string, - _hostname: null as string, - _host: null as string, - _canLaunch: null as boolean, }, ], autofillOnPageLoad: true, diff --git a/libs/common/src/vault/models/view/attachment.view.ts b/libs/common/src/vault/models/view/attachment.view.ts index 1c796c8f275..ef4a9ed8b27 100644 --- a/libs/common/src/vault/models/view/attachment.view.ts +++ b/libs/common/src/vault/models/view/attachment.view.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal"; @@ -10,12 +8,12 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr import { Attachment } from "../domain/attachment"; export class AttachmentView implements View { - id: string = null; - url: string = null; - size: string = null; - sizeName: string = null; - fileName: string = null; - key: SymmetricCryptoKey = null; + id?: string; + url?: string; + size?: string; + sizeName?: string; + fileName?: string; + key?: SymmetricCryptoKey; /** * The SDK returns an encrypted key for the attachment. */ @@ -35,7 +33,7 @@ export class AttachmentView implements View { get fileSize(): number { try { if (this.size != null) { - return parseInt(this.size, null); + return parseInt(this.size); } } catch { // Invalid file size. @@ -71,7 +69,7 @@ export class AttachmentView implements View { fileName: this.fileName, key: this.encryptedKey?.toSdk(), // TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete - decryptedKey: this.key ? this.key.toBase64() : null, + decryptedKey: this.key ? this.key.toBase64() : undefined, }; } @@ -84,13 +82,13 @@ export class AttachmentView implements View { } const view = new AttachmentView(); - view.id = obj.id ?? null; - view.url = obj.url ?? null; - view.size = obj.size ?? null; - view.sizeName = obj.sizeName ?? null; - view.fileName = obj.fileName ?? null; + view.id = obj.id; + view.url = obj.url; + view.size = obj.size; + view.sizeName = obj.sizeName; + view.fileName = obj.fileName; // TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete - view.key = obj.decryptedKey ? SymmetricCryptoKey.fromString(obj.decryptedKey) : null; + view.key = obj.decryptedKey ? SymmetricCryptoKey.fromString(obj.decryptedKey) : undefined; view.encryptedKey = obj.key ? new EncString(obj.key) : undefined; return view; diff --git a/libs/common/src/vault/models/view/card.view.ts b/libs/common/src/vault/models/view/card.view.ts index ed02fa68365..9b78ad384c6 100644 --- a/libs/common/src/vault/models/view/card.view.ts +++ b/libs/common/src/vault/models/view/card.view.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { CardView as SdkCardView } from "@bitwarden/sdk-internal"; @@ -12,45 +10,45 @@ import { ItemView } from "./item.view"; export class CardView extends ItemView implements SdkCardView { @linkedFieldOption(LinkedId.CardholderName, { sortPosition: 0 }) - cardholderName: string = null; + cardholderName: string | undefined; @linkedFieldOption(LinkedId.ExpMonth, { sortPosition: 3, i18nKey: "expirationMonth" }) - expMonth: string = null; + expMonth: string | undefined; @linkedFieldOption(LinkedId.ExpYear, { sortPosition: 4, i18nKey: "expirationYear" }) - expYear: string = null; + expYear: string | undefined; @linkedFieldOption(LinkedId.Code, { sortPosition: 5, i18nKey: "securityCode" }) - code: string = null; + code: string | undefined; - private _brand: string = null; - private _number: string = null; - private _subTitle: string = null; + private _brand?: string; + private _number?: string; + private _subTitle?: string; - get maskedCode(): string { - return this.code != null ? "•".repeat(this.code.length) : null; + get maskedCode(): string | undefined { + return this.code != null ? "•".repeat(this.code.length) : undefined; } - get maskedNumber(): string { - return this.number != null ? "•".repeat(this.number.length) : null; + get maskedNumber(): string | undefined { + return this.number != null ? "•".repeat(this.number.length) : undefined; } @linkedFieldOption(LinkedId.Brand, { sortPosition: 2 }) - get brand(): string { + get brand(): string | undefined { return this._brand; } - set brand(value: string) { + set brand(value: string | undefined) { this._brand = value; - this._subTitle = null; + this._subTitle = undefined; } @linkedFieldOption(LinkedId.Number, { sortPosition: 1 }) - get number(): string { + get number(): string | undefined { return this._number; } - set number(value: string) { + set number(value: string | undefined) { this._number = value; - this._subTitle = null; + this._subTitle = undefined; } - get subTitle(): string { + get subTitle(): string | undefined { if (this._subTitle == null) { this._subTitle = this.brand; if (this.number != null && this.number.length >= 4) { @@ -69,11 +67,11 @@ export class CardView extends ItemView implements SdkCardView { return this._subTitle; } - get expiration(): string { - const normalizedYear = normalizeExpiryYearFormat(this.expYear); + get expiration(): string | undefined { + const normalizedYear = this.expYear ? normalizeExpiryYearFormat(this.expYear) : undefined; if (!this.expMonth && !normalizedYear) { - return null; + return undefined; } let exp = this.expMonth != null ? ("0" + this.expMonth).slice(-2) : "__"; @@ -82,14 +80,14 @@ export class CardView extends ItemView implements SdkCardView { return exp; } - static fromJSON(obj: Partial>): CardView { + static fromJSON(obj: Partial> | undefined): CardView { return Object.assign(new CardView(), obj); } // ref https://stackoverflow.com/a/5911300 - static getCardBrandByPatterns(cardNum: string): string { + static getCardBrandByPatterns(cardNum: string | undefined | null): string | undefined { if (cardNum == null || typeof cardNum !== "string" || cardNum.trim() === "") { - return null; + return undefined; } // Visa @@ -146,25 +144,21 @@ export class CardView extends ItemView implements SdkCardView { return "Visa"; } - return null; + return undefined; } /** * Converts an SDK CardView to a CardView. */ - static fromSdkCardView(obj: SdkCardView): CardView | undefined { - if (obj == null) { - return undefined; - } - + static fromSdkCardView(obj: SdkCardView): CardView { const cardView = new CardView(); - cardView.cardholderName = obj.cardholderName ?? null; - cardView.brand = obj.brand ?? null; - cardView.number = obj.number ?? null; - cardView.expMonth = obj.expMonth ?? null; - cardView.expYear = obj.expYear ?? null; - cardView.code = obj.code ?? null; + cardView.cardholderName = obj.cardholderName; + cardView.brand = obj.brand; + cardView.number = obj.number; + cardView.expMonth = obj.expMonth; + cardView.expYear = obj.expYear; + cardView.code = obj.code; return cardView; } diff --git a/libs/common/src/vault/models/view/cipher.view.spec.ts b/libs/common/src/vault/models/view/cipher.view.spec.ts index e9614db6858..2965a9b1c7f 100644 --- a/libs/common/src/vault/models/view/cipher.view.spec.ts +++ b/libs/common/src/vault/models/view/cipher.view.spec.ts @@ -180,15 +180,12 @@ describe("CipherView", () => { folderId: "folderId", collectionIds: ["collectionId"], name: "name", - notes: null, type: CipherType.Login, favorite: true, edit: true, reprompt: CipherRepromptType.None, organizationUseTotp: false, viewPassword: true, - localData: undefined, - permissions: undefined, attachments: [ { id: "attachmentId", @@ -224,7 +221,6 @@ describe("CipherView", () => { passwordHistory: [], creationDate: new Date("2022-01-01T12:00:00.000Z"), revisionDate: new Date("2022-01-02T12:00:00.000Z"), - deletedDate: null, }); }); }); @@ -283,18 +279,12 @@ describe("CipherView", () => { restore: true, delete: true, }, - deletedDate: undefined, creationDate: "2022-01-02T12:00:00.000Z", revisionDate: "2022-01-02T12:00:00.000Z", attachments: [], passwordHistory: [], - login: undefined, - identity: undefined, - card: undefined, - secureNote: undefined, - sshKey: undefined, fields: [], - } as SdkCipherView); + }); }); }); }); diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index d52e6eb11bd..3381f0a47ab 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -1,7 +1,6 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { asUuid, uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { ItemView } from "@bitwarden/common/vault/models/view/item.view"; import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal"; import { View } from "../../../models/view/view"; @@ -26,18 +25,18 @@ import { SshKeyView } from "./ssh-key.view"; export class CipherView implements View, InitializerMetadata { readonly initializerKey = InitializerKey.CipherView; - id: string = null; - organizationId: string | undefined = null; - folderId: string = null; - name: string = null; - notes: string = null; - type: CipherType = null; + id: string = ""; + organizationId?: string; + folderId?: string; + name: string = ""; + notes?: string; + type: CipherType = CipherType.Login; favorite = false; organizationUseTotp = false; - permissions: CipherPermissionsApi = new CipherPermissionsApi(); + permissions?: CipherPermissionsApi = new CipherPermissionsApi(); edit = false; viewPassword = true; - localData: LocalData; + localData?: LocalData; login = new LoginView(); identity = new IdentityView(); card = new CardView(); @@ -46,11 +45,11 @@ export class CipherView implements View, InitializerMetadata { attachments: AttachmentView[] = []; fields: FieldView[] = []; passwordHistory: PasswordHistoryView[] = []; - collectionIds: string[] = null; - revisionDate: Date = null; - creationDate: Date = null; - deletedDate: Date | null = null; - archivedDate: Date | null = null; + collectionIds: string[] = []; + revisionDate: Date; + creationDate: Date; + deletedDate?: Date; + archivedDate?: Date; reprompt: CipherRepromptType = CipherRepromptType.None; // We need a copy of the encrypted key so we can pass it to // the SdkCipherView during encryption @@ -63,6 +62,7 @@ export class CipherView implements View, InitializerMetadata { constructor(c?: Cipher) { if (!c) { + this.creationDate = this.revisionDate = new Date(); return; } @@ -86,7 +86,7 @@ export class CipherView implements View, InitializerMetadata { this.key = c.key; } - private get item() { + private get item(): ItemView | undefined { switch (this.type) { case CipherType.Login: return this.login; @@ -102,10 +102,10 @@ export class CipherView implements View, InitializerMetadata { break; } - return null; + return undefined; } - get subTitle(): string { + get subTitle(): string | undefined { return this.item?.subTitle; } @@ -114,7 +114,7 @@ export class CipherView implements View, InitializerMetadata { } get hasAttachments(): boolean { - return this.attachments && this.attachments.length > 0; + return !!this.attachments && this.attachments.length > 0; } get hasOldAttachments(): boolean { @@ -132,11 +132,11 @@ export class CipherView implements View, InitializerMetadata { return this.fields && this.fields.length > 0; } - get passwordRevisionDisplayDate(): Date { + get passwordRevisionDisplayDate(): Date | undefined { if (this.type !== CipherType.Login || this.login == null) { - return null; + return undefined; } else if (this.login.password == null || this.login.password === "") { - return null; + return undefined; } return this.login.passwordRevisionDate; } @@ -170,23 +170,17 @@ export class CipherView implements View, InitializerMetadata { * Determines if the cipher can be launched in a new browser tab. */ get canLaunch(): boolean { - return this.type === CipherType.Login && this.login.canLaunch; + return this.type === CipherType.Login && this.login!.canLaunch; } linkedFieldValue(id: LinkedIdType) { const linkedFieldOption = this.linkedFieldOptions?.get(id); - if (linkedFieldOption == null) { - return null; + const item = this.item; + if (linkedFieldOption == null || item == null) { + return undefined; } - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const item = this.item; - return this.item[linkedFieldOption.propertyKey as keyof typeof item]; - } - - linkedFieldI18nKey(id: LinkedIdType): string { - return this.linkedFieldOptions.get(id)?.i18nKey; + return item[linkedFieldOption.propertyKey as keyof typeof item]; } // This is used as a marker to indicate that the cipher view object still has its prototype @@ -194,23 +188,31 @@ export class CipherView implements View, InitializerMetadata { return this; } - static fromJSON(obj: Partial>): CipherView { + static fromJSON(obj: Partial>): CipherView | null { if (obj == null) { return null; } const view = new CipherView(); - const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate); - const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); - const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate); - const archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate); - const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a)); - const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f)); - const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph)); - const permissions = CipherPermissionsApi.fromJSON(obj.permissions); - let key: EncString | undefined; + view.type = obj.type ?? CipherType.Login; + view.id = obj.id ?? ""; + view.name = obj.name ?? ""; + if (obj.creationDate) { + view.creationDate = new Date(obj.creationDate); + } + if (obj.revisionDate) { + view.revisionDate = new Date(obj.revisionDate); + } + view.deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate); + view.archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate); + view.attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a)) ?? []; + view.fields = obj.fields?.map((f: any) => FieldView.fromJSON(f)) ?? []; + view.passwordHistory = + obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph)) ?? []; + view.permissions = obj.permissions ? CipherPermissionsApi.fromJSON(obj.permissions) : undefined; if (obj.key != null) { + let key: EncString | undefined; if (typeof obj.key === "string") { // If the key is a string, we need to parse it as EncString key = EncString.fromJSON(obj.key); @@ -218,20 +220,9 @@ export class CipherView implements View, InitializerMetadata { // If the key is already an EncString instance, we can use it directly key = obj.key; } + view.key = key; } - Object.assign(view, obj, { - creationDate: creationDate, - revisionDate: revisionDate, - deletedDate: deletedDate, - archivedDate: archivedDate, - attachments: attachments, - fields: fields, - passwordHistory: passwordHistory, - permissions: permissions, - key: key, - }); - switch (obj.type) { case CipherType.Card: view.card = CardView.fromJSON(obj.card); @@ -264,46 +255,54 @@ export class CipherView implements View, InitializerMetadata { } const cipherView = new CipherView(); - cipherView.id = uuidAsString(obj.id) ?? null; - cipherView.organizationId = uuidAsString(obj.organizationId) ?? null; - cipherView.folderId = uuidAsString(obj.folderId) ?? null; + cipherView.id = uuidAsString(obj.id); + cipherView.organizationId = uuidAsString(obj.organizationId); + cipherView.folderId = uuidAsString(obj.folderId); cipherView.name = obj.name; - cipherView.notes = obj.notes ?? null; + cipherView.notes = obj.notes; cipherView.type = obj.type; cipherView.favorite = obj.favorite; cipherView.organizationUseTotp = obj.organizationUseTotp; - cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions); + cipherView.permissions = obj.permissions + ? CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions) + : undefined; cipherView.edit = obj.edit; cipherView.viewPassword = obj.viewPassword; cipherView.localData = fromSdkLocalData(obj.localData); cipherView.attachments = - obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? []; - cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? []; + obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)!) ?? []; + cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)!) ?? []; cipherView.passwordHistory = - obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? []; + obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)!) ?? []; cipherView.collectionIds = obj.collectionIds?.map((i) => uuidAsString(i)) ?? []; - cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); - cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate); - cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate); - cipherView.archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate); + cipherView.revisionDate = new Date(obj.revisionDate); + cipherView.creationDate = new Date(obj.creationDate); + cipherView.deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate); + cipherView.archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate); cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None; - cipherView.key = EncString.fromJSON(obj.key); + cipherView.key = obj.key ? EncString.fromJSON(obj.key) : undefined; switch (obj.type) { case CipherType.Card: - cipherView.card = CardView.fromSdkCardView(obj.card); + cipherView.card = obj.card ? CardView.fromSdkCardView(obj.card) : new CardView(); break; case CipherType.Identity: - cipherView.identity = IdentityView.fromSdkIdentityView(obj.identity); + cipherView.identity = obj.identity + ? IdentityView.fromSdkIdentityView(obj.identity) + : new IdentityView(); break; case CipherType.Login: - cipherView.login = LoginView.fromSdkLoginView(obj.login); + cipherView.login = obj.login ? LoginView.fromSdkLoginView(obj.login) : new LoginView(); break; case CipherType.SecureNote: - cipherView.secureNote = SecureNoteView.fromSdkSecureNoteView(obj.secureNote); + cipherView.secureNote = obj.secureNote + ? SecureNoteView.fromSdkSecureNoteView(obj.secureNote) + : new SecureNoteView(); break; case CipherType.SshKey: - cipherView.sshKey = SshKeyView.fromSdkSshKeyView(obj.sshKey); + cipherView.sshKey = obj.sshKey + ? SshKeyView.fromSdkSshKeyView(obj.sshKey) + : new SshKeyView(); break; default: break; @@ -325,11 +324,11 @@ export class CipherView implements View, InitializerMetadata { name: this.name ?? "", notes: this.notes, type: this.type ?? CipherType.Login, - favorite: this.favorite, - organizationUseTotp: this.organizationUseTotp, + favorite: this.favorite ?? false, + organizationUseTotp: this.organizationUseTotp ?? false, permissions: this.permissions?.toSdkCipherPermissions(), - edit: this.edit, - viewPassword: this.viewPassword, + edit: this.edit ?? true, + viewPassword: this.viewPassword ?? true, localData: toSdkLocalData(this.localData), attachments: this.attachments?.map((a) => a.toSdkAttachmentView()), fields: this.fields?.map((f) => f.toSdkFieldView()), @@ -354,19 +353,19 @@ export class CipherView implements View, InitializerMetadata { switch (this.type) { case CipherType.Card: - sdkCipherView.card = this.card.toSdkCardView(); + sdkCipherView.card = this.card?.toSdkCardView(); break; case CipherType.Identity: - sdkCipherView.identity = this.identity.toSdkIdentityView(); + sdkCipherView.identity = this.identity?.toSdkIdentityView(); break; case CipherType.Login: - sdkCipherView.login = this.login.toSdkLoginView(); + sdkCipherView.login = this.login?.toSdkLoginView(); break; case CipherType.SecureNote: - sdkCipherView.secureNote = this.secureNote.toSdkSecureNoteView(); + sdkCipherView.secureNote = this.secureNote?.toSdkSecureNoteView(); break; case CipherType.SshKey: - sdkCipherView.sshKey = this.sshKey.toSdkSshKeyView(); + sdkCipherView.sshKey = this.sshKey?.toSdkSshKeyView(); break; default: break; diff --git a/libs/common/src/vault/models/view/fido2-credential.view.ts b/libs/common/src/vault/models/view/fido2-credential.view.ts index 410757ebe30..19e7f5d7e3c 100644 --- a/libs/common/src/vault/models/view/fido2-credential.view.ts +++ b/libs/common/src/vault/models/view/fido2-credential.view.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { @@ -10,21 +8,55 @@ import { import { ItemView } from "./item.view"; export class Fido2CredentialView extends ItemView { - credentialId: string; - keyType: "public-key"; - keyAlgorithm: "ECDSA"; - keyCurve: "P-256"; - keyValue: string; - rpId: string; - userHandle: string; - userName: string; - counter: number; - rpName: string; - userDisplayName: string; - discoverable: boolean; - creationDate: Date = null; + credentialId!: string; + keyType!: "public-key"; + keyAlgorithm!: "ECDSA"; + keyCurve!: "P-256"; + keyValue!: string; + rpId!: string; + userHandle?: string; + userName?: string; + counter!: number; + rpName?: string; + userDisplayName?: string; + discoverable: boolean = false; + creationDate!: Date; - get subTitle(): string { + constructor(f?: { + credentialId: string; + keyType: "public-key"; + keyAlgorithm: "ECDSA"; + keyCurve: "P-256"; + keyValue: string; + rpId: string; + userHandle?: string; + userName?: string; + counter: number; + rpName?: string; + userDisplayName?: string; + discoverable?: boolean; + creationDate: Date; + }) { + super(); + if (f == null) { + return; + } + this.credentialId = f.credentialId; + this.keyType = f.keyType; + this.keyAlgorithm = f.keyAlgorithm; + this.keyCurve = f.keyCurve; + this.keyValue = f.keyValue; + this.rpId = f.rpId; + this.userHandle = f.userHandle; + this.userName = f.userName; + this.counter = f.counter; + this.rpName = f.rpName; + this.userDisplayName = f.userDisplayName; + this.discoverable = f.discoverable ?? false; + this.creationDate = f.creationDate; + } + + get subTitle(): string | undefined { return this.userDisplayName; } @@ -43,21 +75,21 @@ export class Fido2CredentialView extends ItemView { return undefined; } - const view = new Fido2CredentialView(); - view.credentialId = obj.credentialId; - view.keyType = obj.keyType as "public-key"; - view.keyAlgorithm = obj.keyAlgorithm as "ECDSA"; - view.keyCurve = obj.keyCurve as "P-256"; - view.rpId = obj.rpId; - view.userHandle = obj.userHandle; - view.userName = obj.userName; - view.counter = parseInt(obj.counter); - view.rpName = obj.rpName; - view.userDisplayName = obj.userDisplayName; - view.discoverable = obj.discoverable?.toLowerCase() === "true" ? true : false; - view.creationDate = obj.creationDate ? new Date(obj.creationDate) : null; - - return view; + return new Fido2CredentialView({ + credentialId: obj.credentialId, + keyType: obj.keyType as "public-key", + keyAlgorithm: obj.keyAlgorithm as "ECDSA", + keyCurve: obj.keyCurve as "P-256", + keyValue: obj.keyValue, + rpId: obj.rpId, + userHandle: obj.userHandle, + userName: obj.userName, + counter: parseInt(obj.counter), + rpName: obj.rpName, + userDisplayName: obj.userDisplayName, + discoverable: obj.discoverable?.toLowerCase() === "true", + creationDate: new Date(obj.creationDate), + }); } toSdkFido2CredentialFullView(): Fido2CredentialFullView { diff --git a/libs/common/src/vault/models/view/field.view.ts b/libs/common/src/vault/models/view/field.view.ts index 8c9a923aed2..9f34420a86c 100644 --- a/libs/common/src/vault/models/view/field.view.ts +++ b/libs/common/src/vault/models/view/field.view.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { FieldView as SdkFieldView, FieldType as SdkFieldType } from "@bitwarden/sdk-internal"; @@ -9,13 +7,13 @@ import { FieldType, LinkedIdType } from "../../enums"; import { Field } from "../domain/field"; export class FieldView implements View { - name: string = null; - value: string = null; - type: FieldType = null; + name?: string; + value?: string; + type: FieldType = FieldType.Text; newField = false; // Marks if the field is new and hasn't been saved showValue = false; showCount = false; - linkedId: LinkedIdType = null; + linkedId?: LinkedIdType; constructor(f?: Field) { if (!f) { @@ -26,8 +24,8 @@ export class FieldView implements View { this.linkedId = f.linkedId; } - get maskedValue(): string { - return this.value != null ? "••••••••" : null; + get maskedValue(): string | undefined { + return this.value != null ? "••••••••" : undefined; } static fromJSON(obj: Partial>): FieldView { diff --git a/libs/common/src/vault/models/view/identity.view.ts b/libs/common/src/vault/models/view/identity.view.ts index 2b863dc5e5f..5fb0d1acba5 100644 --- a/libs/common/src/vault/models/view/identity.view.ts +++ b/libs/common/src/vault/models/view/identity.view.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { IdentityView as SdkIdentityView } from "@bitwarden/sdk-internal"; @@ -12,65 +10,65 @@ import { ItemView } from "./item.view"; export class IdentityView extends ItemView implements SdkIdentityView { @linkedFieldOption(LinkedId.Title, { sortPosition: 0 }) - title: string = null; + title: string | undefined; @linkedFieldOption(LinkedId.MiddleName, { sortPosition: 2 }) - middleName: string = null; + middleName: string | undefined; @linkedFieldOption(LinkedId.Address1, { sortPosition: 12 }) - address1: string = null; + address1: string | undefined; @linkedFieldOption(LinkedId.Address2, { sortPosition: 13 }) - address2: string = null; + address2: string | undefined; @linkedFieldOption(LinkedId.Address3, { sortPosition: 14 }) - address3: string = null; + address3: string | undefined; @linkedFieldOption(LinkedId.City, { sortPosition: 15, i18nKey: "cityTown" }) - city: string = null; + city: string | undefined; @linkedFieldOption(LinkedId.State, { sortPosition: 16, i18nKey: "stateProvince" }) - state: string = null; + state: string | undefined; @linkedFieldOption(LinkedId.PostalCode, { sortPosition: 17, i18nKey: "zipPostalCode" }) - postalCode: string = null; + postalCode: string | undefined; @linkedFieldOption(LinkedId.Country, { sortPosition: 18 }) - country: string = null; + country: string | undefined; @linkedFieldOption(LinkedId.Company, { sortPosition: 6 }) - company: string = null; + company: string | undefined; @linkedFieldOption(LinkedId.Email, { sortPosition: 10 }) - email: string = null; + email: string | undefined; @linkedFieldOption(LinkedId.Phone, { sortPosition: 11 }) - phone: string = null; + phone: string | undefined; @linkedFieldOption(LinkedId.Ssn, { sortPosition: 7 }) - ssn: string = null; + ssn: string | undefined; @linkedFieldOption(LinkedId.Username, { sortPosition: 5 }) - username: string = null; + username: string | undefined; @linkedFieldOption(LinkedId.PassportNumber, { sortPosition: 8 }) - passportNumber: string = null; + passportNumber: string | undefined; @linkedFieldOption(LinkedId.LicenseNumber, { sortPosition: 9 }) - licenseNumber: string = null; + licenseNumber: string | undefined; - private _firstName: string = null; - private _lastName: string = null; - private _subTitle: string = null; + private _firstName: string | undefined; + private _lastName: string | undefined; + private _subTitle: string | undefined; constructor() { super(); } @linkedFieldOption(LinkedId.FirstName, { sortPosition: 1 }) - get firstName(): string { + get firstName(): string | undefined { return this._firstName; } - set firstName(value: string) { + set firstName(value: string | undefined) { this._firstName = value; - this._subTitle = null; + this._subTitle = undefined; } @linkedFieldOption(LinkedId.LastName, { sortPosition: 4 }) - get lastName(): string { + get lastName(): string | undefined { return this._lastName; } - set lastName(value: string) { + set lastName(value: string | undefined) { this._lastName = value; - this._subTitle = null; + this._subTitle = undefined; } - get subTitle(): string { + get subTitle(): string | undefined { if (this._subTitle == null && (this.firstName != null || this.lastName != null)) { this._subTitle = ""; if (this.firstName != null) { @@ -88,7 +86,7 @@ export class IdentityView extends ItemView implements SdkIdentityView { } @linkedFieldOption(LinkedId.FullName, { sortPosition: 3 }) - get fullName(): string { + get fullName(): string | undefined { if ( this.title != null || this.firstName != null || @@ -111,11 +109,11 @@ export class IdentityView extends ItemView implements SdkIdentityView { return name.trim(); } - return null; + return undefined; } - get fullAddress(): string { - let address = this.address1; + get fullAddress(): string | undefined { + let address = this.address1 ?? ""; if (!Utils.isNullOrWhitespace(this.address2)) { if (!Utils.isNullOrWhitespace(address)) { address += ", "; @@ -131,9 +129,9 @@ export class IdentityView extends ItemView implements SdkIdentityView { return address; } - get fullAddressPart2(): string { + get fullAddressPart2(): string | undefined { if (this.city == null && this.state == null && this.postalCode == null) { - return null; + return undefined; } const city = this.city || "-"; const state = this.state; @@ -146,7 +144,7 @@ export class IdentityView extends ItemView implements SdkIdentityView { return addressPart2; } - get fullAddressForCopy(): string { + get fullAddressForCopy(): string | undefined { let address = this.fullAddress; if (this.city != null || this.state != null || this.postalCode != null) { address += "\n" + this.fullAddressPart2; @@ -157,38 +155,34 @@ export class IdentityView extends ItemView implements SdkIdentityView { return address; } - static fromJSON(obj: Partial>): IdentityView { + static fromJSON(obj: Partial> | undefined): IdentityView { return Object.assign(new IdentityView(), obj); } /** * Converts the SDK IdentityView to an IdentityView. */ - static fromSdkIdentityView(obj: SdkIdentityView): IdentityView | undefined { - if (obj == null) { - return undefined; - } - + static fromSdkIdentityView(obj: SdkIdentityView): IdentityView { const identityView = new IdentityView(); - identityView.title = obj.title ?? null; - identityView.firstName = obj.firstName ?? null; - identityView.middleName = obj.middleName ?? null; - identityView.lastName = obj.lastName ?? null; - identityView.address1 = obj.address1 ?? null; - identityView.address2 = obj.address2 ?? null; - identityView.address3 = obj.address3 ?? null; - identityView.city = obj.city ?? null; - identityView.state = obj.state ?? null; - identityView.postalCode = obj.postalCode ?? null; - identityView.country = obj.country ?? null; - identityView.company = obj.company ?? null; - identityView.email = obj.email ?? null; - identityView.phone = obj.phone ?? null; - identityView.ssn = obj.ssn ?? null; - identityView.username = obj.username ?? null; - identityView.passportNumber = obj.passportNumber ?? null; - identityView.licenseNumber = obj.licenseNumber ?? null; + identityView.title = obj.title; + identityView.firstName = obj.firstName; + identityView.middleName = obj.middleName; + identityView.lastName = obj.lastName; + identityView.address1 = obj.address1; + identityView.address2 = obj.address2; + identityView.address3 = obj.address3; + identityView.city = obj.city; + identityView.state = obj.state; + identityView.postalCode = obj.postalCode; + identityView.country = obj.country; + identityView.company = obj.company; + identityView.email = obj.email; + identityView.phone = obj.phone; + identityView.ssn = obj.ssn; + identityView.username = obj.username; + identityView.passportNumber = obj.passportNumber; + identityView.licenseNumber = obj.licenseNumber; return identityView; } diff --git a/libs/common/src/vault/models/view/item.view.ts b/libs/common/src/vault/models/view/item.view.ts index 3954276ca04..d25901f8042 100644 --- a/libs/common/src/vault/models/view/item.view.ts +++ b/libs/common/src/vault/models/view/item.view.ts @@ -1,9 +1,7 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { View } from "../../../models/view/view"; import { LinkedMetadata } from "../../linked-field-option.decorator"; export abstract class ItemView implements View { - linkedFieldOptions: Map; - abstract get subTitle(): string; + linkedFieldOptions?: Map; + abstract get subTitle(): string | undefined; } diff --git a/libs/common/src/vault/models/view/login-uri.view.ts b/libs/common/src/vault/models/view/login-uri.view.ts index 49ac9c6278f..bf8dcc83b33 100644 --- a/libs/common/src/vault/models/view/login-uri.view.ts +++ b/libs/common/src/vault/models/view/login-uri.view.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { LoginUriView as SdkLoginUriView } from "@bitwarden/sdk-internal"; @@ -11,13 +9,13 @@ import { Utils } from "../../../platform/misc/utils"; import { LoginUri } from "../domain/login-uri"; export class LoginUriView implements View { - match: UriMatchStrategySetting = null; + match?: UriMatchStrategySetting; - private _uri: string = null; - private _domain: string = null; - private _hostname: string = null; - private _host: string = null; - private _canLaunch: boolean = null; + private _uri?: string; + private _domain?: string; + private _hostname?: string; + private _host?: string; + private _canLaunch?: boolean; constructor(u?: LoginUri) { if (!u) { @@ -27,59 +25,59 @@ export class LoginUriView implements View { this.match = u.match; } - get uri(): string { + get uri(): string | undefined { return this._uri; } - set uri(value: string) { + set uri(value: string | undefined) { this._uri = value; - this._domain = null; - this._canLaunch = null; + this._domain = undefined; + this._canLaunch = undefined; } - get domain(): string { + get domain(): string | undefined { if (this._domain == null && this.uri != null) { this._domain = Utils.getDomain(this.uri); if (this._domain === "") { - this._domain = null; + this._domain = undefined; } } return this._domain; } - get hostname(): string { + get hostname(): string | undefined { if (this.match === UriMatchStrategy.RegularExpression) { - return null; + return undefined; } if (this._hostname == null && this.uri != null) { this._hostname = Utils.getHostname(this.uri); if (this._hostname === "") { - this._hostname = null; + this._hostname = undefined; } } return this._hostname; } - get host(): string { + get host(): string | undefined { if (this.match === UriMatchStrategy.RegularExpression) { - return null; + return undefined; } if (this._host == null && this.uri != null) { this._host = Utils.getHost(this.uri); if (this._host === "") { - this._host = null; + this._host = undefined; } } return this._host; } - get hostnameOrUri(): string { + get hostnameOrUri(): string | undefined { return this.hostname != null ? this.hostname : this.uri; } - get hostOrUri(): string { + get hostOrUri(): string | undefined { return this.host != null ? this.host : this.uri; } @@ -104,7 +102,10 @@ export class LoginUriView implements View { return this._canLaunch; } - get launchUri(): string { + get launchUri(): string | undefined { + if (this.uri == null) { + return undefined; + } return this.uri.indexOf("://") < 0 && !Utils.isNullOrWhitespace(Utils.getDomain(this.uri)) ? "http://" + this.uri : this.uri; @@ -141,7 +142,7 @@ export class LoginUriView implements View { matchesUri( targetUri: string, equivalentDomains: Set, - defaultUriMatch: UriMatchStrategySetting = null, + defaultUriMatch?: UriMatchStrategySetting, /** When present, will override the match strategy for the cipher if it is `Never` with `Domain` */ overrideNeverMatchStrategy?: true, ): boolean { @@ -198,7 +199,7 @@ export class LoginUriView implements View { if (Utils.DomainMatchBlacklist.has(this.domain)) { const domainUrlHost = Utils.getHost(targetUri); - return !Utils.DomainMatchBlacklist.get(this.domain).has(domainUrlHost); + return !Utils.DomainMatchBlacklist.get(this.domain)!.has(domainUrlHost); } return true; diff --git a/libs/common/src/vault/models/view/login.view.spec.ts b/libs/common/src/vault/models/view/login.view.spec.ts index 57e82faf7f1..ec011bed433 100644 --- a/libs/common/src/vault/models/view/login.view.spec.ts +++ b/libs/common/src/vault/models/view/login.view.spec.ts @@ -29,11 +29,6 @@ describe("LoginView", () => { }); describe("fromSdkLoginView", () => { - it("should return undefined when the input is null", () => { - const result = LoginView.fromSdkLoginView(null as unknown as SdkLoginView); - expect(result).toBeUndefined(); - }); - it("should return a LoginView from an SdkLoginView", () => { jest.spyOn(LoginUriView, "fromSdkLoginUriView").mockImplementation(mockFromSdk); diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index 44c6ee8f2e9..6f9167cd777 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal"; import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; @@ -15,15 +13,15 @@ import { LoginUriView } from "./login-uri.view"; export class LoginView extends ItemView { @linkedFieldOption(LinkedId.Username, { sortPosition: 0 }) - username: string = null; + username: string | undefined; @linkedFieldOption(LinkedId.Password, { sortPosition: 1 }) - password: string = null; + password: string | undefined; - passwordRevisionDate?: Date = null; - totp: string = null; + passwordRevisionDate?: Date; + totp: string | undefined; uris: LoginUriView[] = []; - autofillOnPageLoad: boolean = null; - fido2Credentials: Fido2CredentialView[] = null; + autofillOnPageLoad: boolean | undefined; + fido2Credentials: Fido2CredentialView[] = []; constructor(l?: Login) { super(); @@ -35,15 +33,15 @@ export class LoginView extends ItemView { this.autofillOnPageLoad = l.autofillOnPageLoad; } - get uri(): string { - return this.hasUris ? this.uris[0].uri : null; + get uri(): string | undefined { + return this.hasUris ? this.uris[0].uri : undefined; } - get maskedPassword(): string { - return this.password != null ? "••••••••" : null; + get maskedPassword(): string | undefined { + return this.password != null ? "••••••••" : undefined; } - get subTitle(): string { + get subTitle(): string | undefined { // if there's a passkey available, use that as a fallback if (Utils.isNullOrEmpty(this.username) && this.fido2Credentials?.length > 0) { return this.fido2Credentials[0].userName; @@ -60,14 +58,14 @@ export class LoginView extends ItemView { return !Utils.isNullOrWhitespace(this.totp); } - get launchUri(): string { + get launchUri(): string | undefined { if (this.hasUris) { const uri = this.uris.find((u) => u.canLaunch); if (uri != null) { return uri.launchUri; } } - return null; + return undefined; } get hasUris(): boolean { @@ -81,7 +79,7 @@ export class LoginView extends ItemView { matchesUri( targetUri: string, equivalentDomains: Set, - defaultUriMatch: UriMatchStrategySetting = null, + defaultUriMatch?: UriMatchStrategySetting, /** When present, will override the match strategy for the cipher if it is `Never` with `Domain` */ overrideNeverMatchStrategy?: true, ): boolean { @@ -94,17 +92,20 @@ export class LoginView extends ItemView { ); } - static fromJSON(obj: Partial>): LoginView { - const passwordRevisionDate = - obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); - const uris = obj.uris.map((uri) => LoginUriView.fromJSON(uri)); - const fido2Credentials = obj.fido2Credentials?.map((key) => Fido2CredentialView.fromJSON(key)); + static fromJSON(obj: Partial> | undefined): LoginView { + if (obj == undefined) { + return new LoginView(); + } - return Object.assign(new LoginView(), obj, { - passwordRevisionDate, - uris, - fido2Credentials, - }); + const loginView = Object.assign(new LoginView(), obj) as LoginView; + + loginView.passwordRevisionDate = + obj.passwordRevisionDate == null ? undefined : new Date(obj.passwordRevisionDate); + loginView.uris = obj.uris?.map((uri) => LoginUriView.fromJSON(uri)) ?? []; + loginView.fido2Credentials = + obj.fido2Credentials?.map((key) => Fido2CredentialView.fromJSON(key)) ?? []; + + return loginView; } /** @@ -115,25 +116,21 @@ export class LoginView extends ItemView { * the FIDO2 credentials in encrypted form. We can decrypt them later using a separate * call to client.vault().ciphers().decrypt_fido2_credentials(). */ - static fromSdkLoginView(obj: SdkLoginView): LoginView | undefined { - if (obj == null) { - return undefined; - } - + static fromSdkLoginView(obj: SdkLoginView): LoginView { const loginView = new LoginView(); - loginView.username = obj.username ?? null; - loginView.password = obj.password ?? null; + loginView.username = obj.username; + loginView.password = obj.password; loginView.passwordRevisionDate = - obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); - loginView.totp = obj.totp ?? null; - loginView.autofillOnPageLoad = obj.autofillOnPageLoad ?? null; + obj.passwordRevisionDate == null ? undefined : new Date(obj.passwordRevisionDate); + loginView.totp = obj.totp; + loginView.autofillOnPageLoad = obj.autofillOnPageLoad; loginView.uris = obj.uris ?.filter((uri) => uri.uri != null && uri.uri !== "") - .map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || []; + .map((uri) => LoginUriView.fromSdkLoginUriView(uri)!) || []; // FIDO2 credentials are not decrypted here, they remain encrypted - loginView.fido2Credentials = null; + loginView.fido2Credentials = []; return loginView; } diff --git a/libs/common/src/vault/models/view/secure-note.view.ts b/libs/common/src/vault/models/view/secure-note.view.ts index 5e401961869..85c0d3fd61c 100644 --- a/libs/common/src/vault/models/view/secure-note.view.ts +++ b/libs/common/src/vault/models/view/secure-note.view.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { SecureNoteView as SdkSecureNoteView } from "@bitwarden/sdk-internal"; @@ -10,7 +8,7 @@ import { SecureNote } from "../domain/secure-note"; import { ItemView } from "./item.view"; export class SecureNoteView extends ItemView implements SdkSecureNoteView { - type: SecureNoteType = null; + type: SecureNoteType = SecureNoteType.Generic; constructor(n?: SecureNote) { super(); @@ -21,24 +19,20 @@ export class SecureNoteView extends ItemView implements SdkSecureNoteView { this.type = n.type; } - get subTitle(): string { - return null; + get subTitle(): string | undefined { + return undefined; } - static fromJSON(obj: Partial>): SecureNoteView { + static fromJSON(obj: Partial> | undefined): SecureNoteView { return Object.assign(new SecureNoteView(), obj); } /** * Converts the SDK SecureNoteView to a SecureNoteView. */ - static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView | undefined { - if (!obj) { - return undefined; - } - + static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView { const secureNoteView = new SecureNoteView(); - secureNoteView.type = obj.type ?? null; + secureNoteView.type = obj.type; return secureNoteView; } diff --git a/libs/common/src/vault/models/view/ssh-key.view.ts b/libs/common/src/vault/models/view/ssh-key.view.ts index 0547eeb7f8e..525608ce274 100644 --- a/libs/common/src/vault/models/view/ssh-key.view.ts +++ b/libs/common/src/vault/models/view/ssh-key.view.ts @@ -1,24 +1,13 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { SshKeyView as SdkSshKeyView } from "@bitwarden/sdk-internal"; -import { SshKey } from "../domain/ssh-key"; - import { ItemView } from "./item.view"; export class SshKeyView extends ItemView { - privateKey: string = null; - publicKey: string = null; - keyFingerprint: string = null; - - constructor(n?: SshKey) { - super(); - if (!n) { - return; - } - } + privateKey!: string; + publicKey!: string; + keyFingerprint!: string; get maskedPrivateKey(): string { if (!this.privateKey || this.privateKey.length === 0) { @@ -43,23 +32,19 @@ export class SshKeyView extends ItemView { return this.keyFingerprint; } - static fromJSON(obj: Partial>): SshKeyView { + static fromJSON(obj: Partial> | undefined): SshKeyView { return Object.assign(new SshKeyView(), obj); } /** * Converts the SDK SshKeyView to a SshKeyView. */ - static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView | undefined { - if (!obj) { - return undefined; - } - + static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView { const sshKeyView = new SshKeyView(); - sshKeyView.privateKey = obj.privateKey ?? null; - sshKeyView.publicKey = obj.publicKey ?? null; - sshKeyView.keyFingerprint = obj.fingerprint ?? null; + sshKeyView.privateKey = obj.privateKey; + sshKeyView.publicKey = obj.publicKey; + sshKeyView.keyFingerprint = obj.fingerprint; return sshKeyView; } diff --git a/libs/vault/src/services/default-cipher-archive.service.spec.ts b/libs/common/src/vault/services/default-cipher-archive.service.spec.ts similarity index 78% rename from libs/vault/src/services/default-cipher-archive.service.spec.ts rename to libs/common/src/vault/services/default-cipher-archive.service.spec.ts index ec2943ce7e4..972b04d2c4e 100644 --- a/libs/vault/src/services/default-cipher-archive.service.spec.ts +++ b/libs/common/src/vault/services/default-cipher-archive.service.spec.ts @@ -11,21 +11,14 @@ import { CipherBulkArchiveRequest, CipherBulkUnarchiveRequest, } from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { DialogService } from "@bitwarden/components"; import { CipherListView } from "@bitwarden/sdk-internal"; -import { DecryptionFailureDialogComponent } from "../components/decryption-failure-dialog/decryption-failure-dialog.component"; - import { DefaultCipherArchiveService } from "./default-cipher-archive.service"; -import { PasswordRepromptService } from "./password-reprompt.service"; describe("DefaultCipherArchiveService", () => { let service: DefaultCipherArchiveService; let mockCipherService: jest.Mocked; let mockApiService: jest.Mocked; - let mockDialogService: jest.Mocked; - let mockPasswordRepromptService: jest.Mocked; let mockBillingAccountProfileStateService: jest.Mocked; let mockConfigService: jest.Mocked; @@ -35,16 +28,12 @@ describe("DefaultCipherArchiveService", () => { beforeEach(() => { mockCipherService = mock(); mockApiService = mock(); - mockDialogService = mock(); - mockPasswordRepromptService = mock(); mockBillingAccountProfileStateService = mock(); mockConfigService = mock(); service = new DefaultCipherArchiveService( mockCipherService, mockApiService, - mockDialogService, - mockPasswordRepromptService, mockBillingAccountProfileStateService, mockConfigService, ); @@ -244,46 +233,4 @@ describe("DefaultCipherArchiveService", () => { ); }); }); - - describe("canInteract", () => { - let mockCipherView: CipherView; - - beforeEach(() => { - mockCipherView = { - id: cipherId, - decryptionFailure: false, - } as unknown as CipherView; - }); - - it("should return false and open dialog when cipher has decryption failure", async () => { - mockCipherView.decryptionFailure = true; - const openSpy = jest.spyOn(DecryptionFailureDialogComponent, "open").mockImplementation(); - - const result = await service.canInteract(mockCipherView); - - expect(result).toBe(false); - expect(openSpy).toHaveBeenCalledWith(mockDialogService, { - cipherIds: [cipherId], - }); - }); - - it("should return password reprompt result when no decryption failure", async () => { - mockPasswordRepromptService.passwordRepromptCheck.mockResolvedValue(true); - - const result = await service.canInteract(mockCipherView); - - expect(result).toBe(true); - expect(mockPasswordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith( - mockCipherView, - ); - }); - - it("should return false when password reprompt fails", async () => { - mockPasswordRepromptService.passwordRepromptCheck.mockResolvedValue(false); - - const result = await service.canInteract(mockCipherView); - - expect(result).toBe(false); - }); - }); }); diff --git a/libs/vault/src/services/default-cipher-archive.service.ts b/libs/common/src/vault/services/default-cipher-archive.service.ts similarity index 83% rename from libs/vault/src/services/default-cipher-archive.service.ts rename to libs/common/src/vault/services/default-cipher-archive.service.ts index d9a0ec54d73..5c627d687b2 100644 --- a/libs/vault/src/services/default-cipher-archive.service.ts +++ b/libs/common/src/vault/services/default-cipher-archive.service.ts @@ -12,27 +12,21 @@ import { CipherBulkUnarchiveRequest, } from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request"; import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherViewLike, CipherViewLikeUtils, } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; -import { DialogService } from "@bitwarden/components"; import { CipherArchiveService } from "../abstractions/cipher-archive.service"; -import { DecryptionFailureDialogComponent } from "../components/decryption-failure-dialog/decryption-failure-dialog.component"; - -import { PasswordRepromptService } from "./password-reprompt.service"; export class DefaultCipherArchiveService implements CipherArchiveService { constructor( private cipherService: CipherService, private apiService: ApiService, - private dialogService: DialogService, - private passwordRepromptService: PasswordRepromptService, private billingAccountProfileStateService: BillingAccountProfileStateService, private configService: ConfigService, ) {} + /** * Observable that contains the list of ciphers that have been archived. */ @@ -125,21 +119,4 @@ export class DefaultCipherArchiveService implements CipherArchiveService { await this.cipherService.replace(currentCiphers, userId); } - - /** - * Check if the user is able to interact with the cipher - * (password re-prompt / decryption failure checks). - * @param cipher - * @private - */ - async canInteract(cipher: CipherView) { - if (cipher.decryptionFailure) { - DecryptionFailureDialogComponent.open(this.dialogService, { - cipherIds: [cipher.id as CipherId], - }); - return false; - } - - return await this.passwordRepromptService.passwordRepromptCheck(cipher); - } } diff --git a/libs/common/src/vault/services/search.service.ts b/libs/common/src/vault/services/search.service.ts index cbd89cf1ab1..80fddda42d5 100644 --- a/libs/common/src/vault/services/search.service.ts +++ b/libs/common/src/vault/services/search.service.ts @@ -296,12 +296,20 @@ export class SearchService implements SearchServiceAbstraction { return results; } - searchCiphersBasic(ciphers: C[], query: string, deleted = false) { + searchCiphersBasic( + ciphers: C[], + query: string, + deleted = false, + archived = false, + ) { query = SearchService.normalizeSearchQuery(query.trim().toLowerCase()); return ciphers.filter((c) => { if (deleted !== CipherViewLikeUtils.isDeleted(c)) { return false; } + if (archived !== CipherViewLikeUtils.isArchived(c)) { + return false; + } if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) { return true; } diff --git a/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts b/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts index 70122ebd27b..56b94fcf3ce 100644 --- a/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts +++ b/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts @@ -298,6 +298,10 @@ describe("CipherViewLikeUtils", () => { (cipherView.attachments as any) = null; expect(CipherViewLikeUtils.hasAttachments(cipherView)).toBe(false); + + cipherView.attachments = []; + + expect(CipherViewLikeUtils.hasAttachments(cipherView)).toBe(false); }); }); diff --git a/libs/common/src/vault/utils/observable-utilities.ts b/libs/common/src/vault/utils/observable-utilities.ts index cdec51fc953..025da8a36f0 100644 --- a/libs/common/src/vault/utils/observable-utilities.ts +++ b/libs/common/src/vault/utils/observable-utilities.ts @@ -30,7 +30,7 @@ export function perUserCache$( create(userId), clearBuffer$.pipe( filter((clearId) => clearId === userId || clearId === null), - map(() => null), + map((): any => null), ), ).pipe(shareReplay({ bufferSize: 1, refCount: false })); cache.set(userId, observable); diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index 6f83c9ca101..8ece033c73d 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -1,6 +1,5 @@ import { NgClass } from "@angular/common"; -import { Component, OnChanges, input } from "@angular/core"; -import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; +import { Component, computed, input } from "@angular/core"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -22,12 +21,34 @@ const SizeClasses: Record = { */ @Component({ selector: "bit-avatar", - template: `@if (src) { - - }`, + template: ` + + + + {{ displayChars() }} + + + + `, imports: [NgClass], }) -export class AvatarComponent implements OnChanges { +export class AvatarComponent { readonly border = input(false); readonly color = input(); readonly id = input(); @@ -35,36 +56,40 @@ export class AvatarComponent implements OnChanges { readonly title = input(); readonly size = input("default"); - private svgCharCount = 2; - private svgFontSize = 20; - private svgFontWeight = 300; - private svgSize = 48; - src?: SafeResourceUrl; + protected readonly svgCharCount = 2; + protected readonly svgFontSize = 20; + protected readonly svgFontWeight = 300; + protected readonly svgSize = 48; - constructor(public sanitizer: DomSanitizer) {} - - ngOnChanges() { - this.generate(); - } - - get classList() { - return ["tw-rounded-full", "tw-inline"] + protected readonly classList = computed(() => { + return ["tw-rounded-full"] .concat(SizeClasses[this.size()] ?? []) .concat(this.border() ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []); - } + }); - private generate() { - const color = this.color(); - const text = this.text(); + protected readonly backgroundColor = computed(() => { const id = this.id(); - if (!text && !color && !id) { - throw new Error("Must supply `text`, `color`, or `id` input."); + const upperCaseText = this.text()?.toUpperCase() ?? ""; + + if (!Utils.isNullOrWhitespace(this.color())) { + return this.color()!; } - let chars: string | null = null; - const upperCaseText = text?.toUpperCase() ?? ""; - chars = this.getFirstLetters(upperCaseText, this.svgCharCount); + if (!Utils.isNullOrWhitespace(id)) { + return Utils.stringToColor(id!.toString()); + } + return Utils.stringToColor(upperCaseText); + }); + + protected readonly textColor = computed(() => { + return Utils.pickTextColorBasedOnBgColor(this.backgroundColor(), 135, true); + }); + + protected readonly displayChars = computed(() => { + const upperCaseText = this.text()?.toUpperCase() ?? ""; + + let chars = this.getFirstLetters(upperCaseText, this.svgCharCount); if (chars == null) { chars = this.unicodeSafeSubstring(upperCaseText, this.svgCharCount); } @@ -75,30 +100,10 @@ export class AvatarComponent implements OnChanges { chars = emojiMatch[0]; } - let svg: HTMLElement; - let hexColor = color ?? ""; - if (!Utils.isNullOrWhitespace(hexColor)) { - svg = this.createSvgElement(this.svgSize, hexColor); - } else if (!Utils.isNullOrWhitespace(id ?? "")) { - hexColor = Utils.stringToColor(id!.toString()); - svg = this.createSvgElement(this.svgSize, hexColor); - } else { - hexColor = Utils.stringToColor(upperCaseText); - svg = this.createSvgElement(this.svgSize, hexColor); - } + return chars; + }); - const charObj = this.createTextElement(chars, hexColor); - svg.appendChild(charObj); - const html = window.document.createElement("div").appendChild(svg).outerHTML; - const svgHtml = window.btoa(unescape(encodeURIComponent(html))); - - // This is safe because the only user provided value, chars is set using `textContent` - this.src = this.sanitizer.bypassSecurityTrustResourceUrl( - "data:image/svg+xml;base64," + svgHtml, - ); - } - - private getFirstLetters(data: string, count: number): string | null { + private getFirstLetters(data: string, count: number): string | undefined { const parts = data.split(" "); if (parts.length > 1) { let text = ""; @@ -107,39 +112,7 @@ export class AvatarComponent implements OnChanges { } return text; } - return null; - } - - private createSvgElement(size: number, color: string): HTMLElement { - const svgTag = window.document.createElement("svg"); - svgTag.setAttribute("xmlns", "http://www.w3.org/2000/svg"); - svgTag.setAttribute("pointer-events", "none"); - svgTag.setAttribute("width", size.toString()); - svgTag.setAttribute("height", size.toString()); - svgTag.style.backgroundColor = color; - svgTag.style.width = size + "px"; - svgTag.style.height = size + "px"; - return svgTag; - } - - private createTextElement(character: string, color: string): HTMLElement { - const textTag = window.document.createElement("text"); - textTag.setAttribute("text-anchor", "middle"); - textTag.setAttribute("y", "50%"); - textTag.setAttribute("x", "50%"); - textTag.setAttribute("dy", "0.35em"); - textTag.setAttribute("pointer-events", "auto"); - textTag.setAttribute("fill", Utils.pickTextColorBasedOnBgColor(color, 135, true)); - textTag.setAttribute( - "font-family", - 'Roboto,"Helvetica Neue",Helvetica,Arial,' + - 'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"', - ); - // Warning do not use innerHTML here, characters are user provided - textTag.textContent = character; - textTag.style.fontWeight = this.svgFontWeight.toString(); - textTag.style.fontSize = this.svgFontSize + "px"; - return textTag; + return undefined; } private unicodeSafeSubstring(str: string, count: number) { diff --git a/libs/components/src/callout/callout.component.html b/libs/components/src/callout/callout.component.html index b98679766d5..e0fe0a182ea 100644 --- a/libs/components/src/callout/callout.component.html +++ b/libs/components/src/callout/callout.component.html @@ -1,24 +1,26 @@ diff --git a/libs/components/src/callout/callout.component.spec.ts b/libs/components/src/callout/callout.component.spec.ts index b7dfe29a643..e052395067d 100644 --- a/libs/components/src/callout/callout.component.spec.ts +++ b/libs/components/src/callout/callout.component.spec.ts @@ -56,5 +56,12 @@ describe("Callout", () => { expect(component.titleComputed()).toBe("Error"); expect(component.iconComputed()).toBe("bwi-error"); }); + + it("default", () => { + fixture.componentRef.setInput("type", "default"); + fixture.detectChanges(); + expect(component.titleComputed()).toBeUndefined(); + expect(component.iconComputed()).toBe("bwi-star"); + }); }); }); diff --git a/libs/components/src/callout/callout.component.ts b/libs/components/src/callout/callout.component.ts index 99a6c2aa123..62321a34d91 100644 --- a/libs/components/src/callout/callout.component.ts +++ b/libs/components/src/callout/callout.component.ts @@ -5,13 +5,14 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { SharedModule } from "../shared"; import { TypographyModule } from "../typography"; -export type CalloutTypes = "success" | "info" | "warning" | "danger"; +export type CalloutTypes = "success" | "info" | "warning" | "danger" | "default"; const defaultIcon: Record = { success: "bwi-check-circle", info: "bwi-info-circle", warning: "bwi-exclamation-triangle", danger: "bwi-error", + default: "bwi-star", }; const defaultI18n: Partial> = { @@ -55,13 +56,15 @@ export class CalloutComponent { protected readonly calloutClass = computed(() => { switch (this.type()) { case "danger": - return "tw-bg-danger-100"; + return "tw-bg-danger-100 tw-border-danger-700 tw-text-danger-700"; case "info": - return "tw-bg-info-100"; + return "tw-bg-info-100 tw-bg-info-100 tw-border-info-700 tw-text-info-700"; case "success": - return "tw-bg-success-100"; + return "tw-bg-success-100 tw-bg-success-100 tw-border-success-700 tw-text-success-700"; case "warning": - return "tw-bg-warning-100"; + return "tw-bg-warning-100 tw-bg-warning-100 tw-border-warning-700 tw-text-warning-700"; + case "default": + return "tw-bg-background-alt tw-border-secondary-700 tw-text-secondary-700"; } }); } diff --git a/libs/components/src/callout/callout.mdx b/libs/components/src/callout/callout.mdx index a1254b3f691..297a2ffd0a3 100644 --- a/libs/components/src/callout/callout.mdx +++ b/libs/components/src/callout/callout.mdx @@ -41,6 +41,12 @@ automatically be checked. +### Default + +Use for similar cases as the info callout but when content does not need to be as prominent. + + + ### Warning Use a warning callout if the user is about to perform an action that may have unintended or @@ -67,4 +73,8 @@ Use the `role=”alert”` only if the callout is appearing on a page after the the content is static, do not use the alert role. This will cause a screen reader to announce the callout content on page load. -Ensure the title's color contrast remains WCAG compliant with the callout's background. +Ensure color contrast remains WCAG compliant with the callout's background. This is especially +important when adding `bit-link` or `bit-button` to the content area since the callout background is +colored. Currently only the `info` and `default` callouts are WCAG compliant for the `primary` +styling of these elements. The `secondary` `bit-link` styling may be used with the remaining +variants. diff --git a/libs/components/src/callout/callout.stories.ts b/libs/components/src/callout/callout.stories.ts index 4ac4191ce7e..c2185203034 100644 --- a/libs/components/src/callout/callout.stories.ts +++ b/libs/components/src/callout/callout.stories.ts @@ -1,6 +1,7 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LinkModule, IconModule } from "@bitwarden/components"; import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet"; import { I18nMockService } from "../utils/i18n-mock.service"; @@ -12,6 +13,7 @@ export default { component: CalloutComponent, decorators: [ moduleMetadata({ + imports: [LinkModule, IconModule], providers: [ { provide: I18nService, @@ -69,6 +71,14 @@ export const Danger: Story = { }, }; +export const Default: Story = { + ...Info, + args: { + ...Info.args, + type: "default", + }, +}; + export const CustomIcon: Story = { ...Info, args: { @@ -80,6 +90,35 @@ export const CustomIcon: Story = { export const NoTitle: Story = { ...Info, args: { - icon: "bwi-star", + icon: "", + }, +}; + +export const NoTitleWithIcon: Story = { + render: (args) => ({ + props: args, + template: ` + (args)}>The content of the callout + `, + }), + args: { + type: "default", + icon: "bwi-globe", + }, +}; + +export const WithTextButton: Story = { + render: (args) => ({ + props: args, + template: ` + (args)}> +

The content of the callout

+ Visit the help center +
+ `, + }), + args: { + type: "default", + icon: "", }, }; diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 1b701a50584..030879deabd 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -20,7 +20,7 @@ bitDialogTitleContainer bitTypography="h3" noMargin - class="tw-text-main tw-mb-0 tw-truncate" + class="tw-text-main tw-mb-0 tw-line-clamp-2 tw-text-ellipsis tw-break-words" > {{ title() }} @if (subtitle(); as subtitleText) { diff --git a/libs/components/src/dialog/dialog/dialog.stories.ts b/libs/components/src/dialog/dialog/dialog.stories.ts index f93ef1a2f25..d645d32764d 100644 --- a/libs/components/src/dialog/dialog/dialog.stories.ts +++ b/libs/components/src/dialog/dialog/dialog.stories.ts @@ -125,7 +125,15 @@ export const LongTitle: Story = { ...Default, args: { dialogSize: "small", - title: "Long_Title_That_Should_Be_Truncated", + title: "Incredibly_Super_Long_Title_That_Should_Be_Truncated", + }, +}; + +export const LongTitleSentence: Story = { + ...Default, + args: { + dialogSize: "small", + title: "Very Long Sentence That Should Be Truncated After Two Lines", }, }; diff --git a/libs/components/src/form-field/form-field.component.html b/libs/components/src/form-field/form-field.component.html index ae3bad40698..c2c92104727 100644 --- a/libs/components/src/form-field/form-field.component.html +++ b/libs/components/src/form-field/form-field.component.html @@ -56,7 +56,7 @@
{ return new I18nMockService({ close: "Close", + loading: "Loading", }); }, }, diff --git a/libs/components/src/tooltip/index.ts b/libs/components/src/tooltip/index.ts new file mode 100644 index 00000000000..28c35fd6ee6 --- /dev/null +++ b/libs/components/src/tooltip/index.ts @@ -0,0 +1 @@ +export * from "./tooltip.directive"; diff --git a/libs/components/src/tooltip/tooltip-positions.ts b/libs/components/src/tooltip/tooltip-positions.ts new file mode 100644 index 00000000000..6396bb6632e --- /dev/null +++ b/libs/components/src/tooltip/tooltip-positions.ts @@ -0,0 +1,61 @@ +import { ConnectedPosition } from "@angular/cdk/overlay"; + +const ORIGIN_OFFSET_PX = 10; + +export type TooltipPositionIdentifier = + | "right-center" + | "left-center" + | "below-center" + | "above-center"; + +export interface TooltipPosition extends ConnectedPosition { + id: TooltipPositionIdentifier; +} + +export const tooltipPositions: TooltipPosition[] = [ + /** + * The order of these positions matters. The Tooltip component will use + * the first position that fits within the viewport. + */ + + // Tooltip opens to right of trigger + { + id: "right-center", + offsetX: ORIGIN_OFFSET_PX, + originX: "end", + originY: "center", + overlayX: "start", + overlayY: "center", + panelClass: ["bit-tooltip-right-center"], + }, + // ... to left of trigger + { + id: "left-center", + offsetX: -ORIGIN_OFFSET_PX, + originX: "start", + originY: "center", + overlayX: "end", + overlayY: "center", + panelClass: ["bit-tooltip-left-center"], + }, + // ... below trigger + { + id: "below-center", + offsetY: ORIGIN_OFFSET_PX, + originX: "center", + originY: "bottom", + overlayX: "center", + overlayY: "top", + panelClass: ["bit-tooltip-below-center"], + }, + // ... above trigger + { + id: "above-center", + offsetY: -ORIGIN_OFFSET_PX, + originX: "center", + originY: "top", + overlayX: "center", + overlayY: "bottom", + panelClass: ["bit-tooltip-above-center"], + }, +]; diff --git a/libs/components/src/tooltip/tooltip.component.css b/libs/components/src/tooltip/tooltip.component.css new file mode 100644 index 00000000000..4abb9908f25 --- /dev/null +++ b/libs/components/src/tooltip/tooltip.component.css @@ -0,0 +1,132 @@ +:root { + --tooltip-shadow: rgb(0 0 0 / 0.1); +} + +.cdk-overlay-pane:has(.bit-tooltip[data-visible="false"]) { + pointer-events: none; +} + +.bit-tooltip-container { + position: relative; + max-width: 12rem; + opacity: 0; + width: max-content; + box-shadow: + 0 4px 6px -1px var(--tooltip-shadow), + 0 2px 4px -2px var(--tooltip-shadow); + border-radius: 0.75rem; + transition: + transform 100ms ease-in-out, + opacity 100ms ease-in-out; + transform: scale(0.95); + z-index: 1; + + &::before, + &::after { + content: ""; + position: absolute; + width: 1rem; + height: 1rem; + z-index: 1; + rotate: 45deg; + border-radius: 3px; + } + + &::before { + background: linear-gradient(135deg, transparent 50%, rgb(var(--color-text-main)) 50%); + z-index: -1; + } + + &::after { + background: rgb(var(--color-text-main)); + z-index: -1; + } + + &[data-visible="true"] { + opacity: 1; + transform: scale(1); + z-index: 1000; + } + + .bit-tooltip-above-center &, + .bit-tooltip-below-center & { + &::before, + &::after { + inset-inline-start: 50%; + transform: translateX(-50%); + transform-origin: left; + } + } + + .bit-tooltip-above-center & { + &::after { + filter: drop-shadow(0 3px 5px var(--tooltip-shadow)) + drop-shadow(0 1px 3px var(--tooltip-shadow)); + } + + &::before, + &::after { + inset-block-end: -0.25rem; + } + } + + .bit-tooltip-below-center & { + &::after { + display: none; + } + + &::after, + &::before { + inset-block-start: -0.25rem; + rotate: -135deg; + } + } + + .bit-tooltip-left-center &, + .bit-tooltip-right-center & { + &::after, + &::before { + inset-block-start: 50%; + transform: translateY(-50%); + transform-origin: top; + } + } + + .bit-tooltip-left-center & { + &::after { + filter: drop-shadow(-3px 1px 3px var(--tooltip-shadow)) + drop-shadow(-1px 2px 3px var(--tooltip-shadow)); + } + + &::after, + &::before { + inset-inline-end: -0.25rem; + rotate: -45deg; + } + } + + .bit-tooltip-right-center & { + &::after { + filter: drop-shadow(2px -4px 2px var(--tooltip-shadow)) + drop-shadow(0 -1px 3px var(--tooltip-shadow)); + } + + &::after, + &::before { + inset-inline-start: -0.25rem; + rotate: 135deg; + } + } +} + +.bit-tooltip { + width: max-content; + max-width: 12rem; + background-color: rgb(var(--color-text-main)); + color: rgb(var(--color-text-contrast)); + padding: 0.5rem 0.75rem; + border-radius: 0.75rem; + font-size: 0.875rem; + line-height: 1.25rem; + z-index: 2; +} diff --git a/libs/components/src/tooltip/tooltip.component.html b/libs/components/src/tooltip/tooltip.component.html new file mode 100644 index 00000000000..c75cd5fb0d4 --- /dev/null +++ b/libs/components/src/tooltip/tooltip.component.html @@ -0,0 +1,10 @@ + +
+ +
diff --git a/libs/components/src/tooltip/tooltip.component.ts b/libs/components/src/tooltip/tooltip.component.ts new file mode 100644 index 00000000000..6b240507311 --- /dev/null +++ b/libs/components/src/tooltip/tooltip.component.ts @@ -0,0 +1,36 @@ +import { CommonModule } from "@angular/common"; +import { + Component, + ElementRef, + inject, + InjectionToken, + Signal, + TemplateRef, + viewChild, +} from "@angular/core"; + +import { TooltipPosition } from "./tooltip-positions"; + +type TooltipData = { + content: Signal; + isVisible: Signal; + tooltipPosition: Signal; +}; + +export const TOOLTIP_DATA = new InjectionToken("TOOLTIP_DATA"); + +/** + * tooltip component used internally by the tooltip.directive. Not meant to be used explicitly + */ +@Component({ + selector: "bit-tooltip", + templateUrl: "./tooltip.component.html", + imports: [CommonModule], +}) +export class TooltipComponent { + readonly templateRef = viewChild.required(TemplateRef); + + private elementRef = inject(ElementRef); + + readonly tooltipData = inject(TOOLTIP_DATA); +} diff --git a/libs/components/src/tooltip/tooltip.directive.ts b/libs/components/src/tooltip/tooltip.directive.ts new file mode 100644 index 00000000000..153fecfe7bf --- /dev/null +++ b/libs/components/src/tooltip/tooltip.directive.ts @@ -0,0 +1,110 @@ +import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay"; +import { ComponentPortal } from "@angular/cdk/portal"; +import { + Directive, + ViewContainerRef, + inject, + OnInit, + ElementRef, + Injector, + input, + effect, + signal, +} from "@angular/core"; + +import { TooltipPositionIdentifier, tooltipPositions } from "./tooltip-positions"; +import { TooltipComponent, TOOLTIP_DATA } from "./tooltip.component"; + +/** + * Directive to add a tooltip to any element. The tooltip content is provided via the `bitTooltip` input. + * The position of the tooltip can be set via the `tooltipPosition` input. Default position is "above-center". + */ +@Directive({ + selector: "[bitTooltip]", + host: { + "(mouseenter)": "showTooltip()", + "(mouseleave)": "hideTooltip()", + "(focus)": "showTooltip()", + "(blur)": "hideTooltip()", + }, +}) +export class TooltipDirective implements OnInit { + /** + * The value of this input is forwarded to the tooltip.component to render + */ + readonly bitTooltip = input.required(); + /** + * The value of this input is forwarded to the tooltip.component to set its position explicitly. + * @default "above-center" + */ + readonly tooltipPosition = input("above-center"); + + private isVisible = signal(false); + private overlayRef: OverlayRef | undefined; + private elementRef = inject(ElementRef); + private overlay = inject(Overlay); + private viewContainerRef = inject(ViewContainerRef); + private injector = inject(Injector); + private positionStrategy = this.overlay + .position() + .flexibleConnectedTo(this.elementRef) + .withFlexibleDimensions(false) + .withPush(true); + + private tooltipPortal = new ComponentPortal( + TooltipComponent, + this.viewContainerRef, + Injector.create({ + providers: [ + { + provide: TOOLTIP_DATA, + useValue: { + content: this.bitTooltip, + isVisible: this.isVisible, + tooltipPosition: this.tooltipPosition, + }, + }, + ], + }), + ); + + private showTooltip = () => { + this.isVisible.set(true); + }; + + private hideTooltip = () => { + this.isVisible.set(false); + }; + + private computePositions(tooltipPosition: TooltipPositionIdentifier) { + const chosenPosition = tooltipPositions.find((position) => position.id === tooltipPosition); + + return chosenPosition ? [chosenPosition, ...tooltipPositions] : tooltipPositions; + } + + get defaultPopoverConfig(): OverlayConfig { + return { + hasBackdrop: false, + scrollStrategy: this.overlay.scrollStrategies.reposition(), + }; + } + + ngOnInit() { + this.positionStrategy.withPositions(this.computePositions(this.tooltipPosition())); + + this.overlayRef = this.overlay.create({ + ...this.defaultPopoverConfig, + positionStrategy: this.positionStrategy, + }); + + this.overlayRef.attach(this.tooltipPortal); + + effect( + () => { + this.positionStrategy.withPositions(this.computePositions(this.tooltipPosition())); + this.overlayRef?.updatePosition(); + }, + { injector: this.injector }, + ); + } +} diff --git a/libs/components/src/tooltip/tooltip.mdx b/libs/components/src/tooltip/tooltip.mdx new file mode 100644 index 00000000000..4b6f10d97f8 --- /dev/null +++ b/libs/components/src/tooltip/tooltip.mdx @@ -0,0 +1,31 @@ +import { Meta, Canvas, Source, Primary, Controls, Title, Description } from "@storybook/addon-docs"; + +import * as stories from "./tooltip.stories"; + + + +```ts +import { TooltipDirective } from "@bitwarden/components"; +``` + + +<Description /> + +NOTE: The `TooltipComponent` can't be used on its own. It must be applied via the `TooltipDirective` + +<Primary /> +<Controls /> + +## Stories + +### All available positions + +<Canvas of={stories.AllPositions} /> + +### Used with a long content + +<Canvas of={stories.LongContent} /> + +### On disabled element + +<Canvas of={stories.OnDisabledButton} /> diff --git a/libs/components/src/tooltip/tooltip.spec.ts b/libs/components/src/tooltip/tooltip.spec.ts new file mode 100644 index 00000000000..57e05e4f65f --- /dev/null +++ b/libs/components/src/tooltip/tooltip.spec.ts @@ -0,0 +1,103 @@ +import { + ConnectedOverlayPositionChange, + ConnectionPositionPair, + OverlayConfig, + Overlay, +} from "@angular/cdk/overlay"; +import { ComponentPortal } from "@angular/cdk/portal"; +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { Observable, Subject } from "rxjs"; + +import { TooltipDirective } from "./tooltip.directive"; + +@Component({ + standalone: true, + imports: [TooltipDirective], + template: ` <button [bitTooltip]="tooltipText" type="button">Hover or focus me</button> `, +}) +class TooltipHostComponent { + tooltipText = "Hello Tooltip"; +} + +/** Minimal strategy shape the directive expects */ +interface StrategyLike { + withFlexibleDimensions: (flex: boolean) => StrategyLike; + withPush: (push: boolean) => StrategyLike; + withPositions: (positions: ReadonlyArray<ConnectionPositionPair>) => StrategyLike; + readonly positionChanges: Observable<ConnectedOverlayPositionChange>; +} + +/** Minimal Overlay service shape */ +interface OverlayLike { + position: () => { flexibleConnectedTo: (_: unknown) => StrategyLike }; + create: (_: OverlayConfig) => OverlayRefStub; + scrollStrategies: { reposition: () => unknown }; +} + +interface OverlayRefStub { + attach: (portal: ComponentPortal<unknown>) => unknown; + updatePosition: () => void; +} + +describe("TooltipDirective (visibility only)", () => { + let fixture: ComponentFixture<TooltipHostComponent>; + + beforeEach(() => { + const positionChanges$ = new Subject<ConnectedOverlayPositionChange>(); + + const strategy: StrategyLike = { + withFlexibleDimensions: jest.fn(() => strategy), + withPush: jest.fn(() => strategy), + withPositions: jest.fn(() => strategy), + get positionChanges() { + return positionChanges$.asObservable(); + }, + }; + + const overlayRefStub: OverlayRefStub = { + attach: jest.fn(() => ({})), + updatePosition: jest.fn(), + }; + + const overlayMock: OverlayLike = { + position: () => ({ flexibleConnectedTo: () => strategy }), + create: (_: OverlayConfig) => overlayRefStub, + scrollStrategies: { reposition: () => ({}) }, + }; + + TestBed.configureTestingModule({ + imports: [TooltipHostComponent], + providers: [{ provide: Overlay, useValue: overlayMock as unknown as Overlay }], + }); + + fixture = TestBed.createComponent(TooltipHostComponent); + fixture.detectChanges(); + }); + + function getDirective(): TooltipDirective { + const hostDE = fixture.debugElement.query(By.directive(TooltipDirective)); + return hostDE.injector.get(TooltipDirective); + } + + it("sets isVisible to true on mouseenter", () => { + const button: HTMLButtonElement = fixture.debugElement.query(By.css("button")).nativeElement; + const directive = getDirective(); + + const isVisible = (directive as unknown as { isVisible: () => boolean }).isVisible; + + button.dispatchEvent(new Event("mouseenter")); + expect(isVisible()).toBe(true); + }); + + it("sets isVisible to true on focus", () => { + const button: HTMLButtonElement = fixture.debugElement.query(By.css("button")).nativeElement; + const directive = getDirective(); + + const isVisible = (directive as unknown as { isVisible: () => boolean }).isVisible; + + button.dispatchEvent(new Event("focus")); + expect(isVisible()).toBe(true); + }); +}); diff --git a/libs/components/src/tooltip/tooltip.stories.ts b/libs/components/src/tooltip/tooltip.stories.ts new file mode 100644 index 00000000000..8ea3f52f913 --- /dev/null +++ b/libs/components/src/tooltip/tooltip.stories.ts @@ -0,0 +1,153 @@ +import { signal } from "@angular/core"; +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +import { getByRole, userEvent } from "@storybook/test"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { ButtonComponent } from "../button"; +import { BitIconButtonComponent } from "../icon-button"; +import { I18nMockService } from "../utils"; + +import { TooltipPosition, TooltipPositionIdentifier, tooltipPositions } from "./tooltip-positions"; +import { TOOLTIP_DATA, TooltipComponent } from "./tooltip.component"; +import { TooltipDirective } from "./tooltip.directive"; + +import { formatArgsForCodeSnippet } from ".storybook/format-args-for-code-snippet"; + +export default { + title: "Component Library/Tooltip", + component: TooltipDirective, + decorators: [ + moduleMetadata({ + imports: [TooltipDirective, TooltipComponent, BitIconButtonComponent, ButtonComponent], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + loading: "Loading", + }); + }, + }, + { + provide: TOOLTIP_DATA, + useFactory: () => { + // simple fixed demo values for the Default story + return { + content: signal("This is a tooltip"), + isVisible: signal(true), + tooltipPosition: signal<TooltipPositionIdentifier>("above-center"), + }; + }, + }, + ], + }), + ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?m=auto&node-id=30558-13730&t=4k23PtzCwqDekAZW-1", + }, + }, + argTypes: { + bitTooltip: { + control: "text", + description: "Text content of the tooltip", + }, + tooltipPosition: { + control: "select", + options: tooltipPositions.map((position: TooltipPosition) => position.id), + description: "Position of the tooltip relative to the element", + table: { + type: { + summary: tooltipPositions.map((position: TooltipPosition) => position.id).join(" | "), + }, + defaultValue: { summary: "above-center" }, + }, + }, + }, +} as Meta<TooltipDirective>; + +type Story = StoryObj<TooltipDirective>; + +export const Default: Story = { + args: { + bitTooltip: "This is a tooltip", + tooltipPosition: "above-center", + }, + render: (args) => ({ + props: args, + template: ` + <div class="tw-p-4"> + <button + bitIconButton="bwi-ellipsis-v" + ${formatArgsForCodeSnippet<TooltipDirective>(args)} + > + Button label here + </button> + </div> + `, + }), + play: async (context) => { + const canvasEl = context.canvasElement; + const button = getByRole(canvasEl, "button"); + + await userEvent.hover(button); + }, +}; + +export const AllPositions: Story = { + render: () => ({ + template: ` + <div class="tw-p-16 tw-grid tw-grid-cols-2 tw-gap-8 tw-place-items-center"> + <button + bitIconButton="bwi-angle-up" + bitTooltip="Top tooltip" + tooltipPosition="above-center" + ></button> + <button + bitIconButton="bwi-angle-right" + bitTooltip="Right tooltip" + tooltipPosition="right-center" + ></button> + <button + bitIconButton="bwi-angle-left" + bitTooltip="Left tooltip" + tooltipPosition="left-center" + ></button> + <button + bitIconButton="bwi-angle-down" + bitTooltip="Bottom tooltip" + tooltipPosition="below-center" + ></button> + </div> + `, + }), +}; + +export const LongContent: Story = { + render: () => ({ + template: ` + <div class="tw-p-16 tw-flex tw-items-center tw-justify-center"> + <button + bitIconButton="bwi-ellipsis-v" + bitTooltip="This is a very long tooltip that will wrap to multiple lines to demonstrate how the tooltip handles long content. This is not recommended for usability." + ></button> + </div> + `, + }), +}; + +export const OnDisabledButton: Story = { + render: () => ({ + template: ` + <div class="tw-p-16 tw-flex tw-items-center tw-justify-center"> + <button + bitIconButton="bwi-ellipsis-v" + bitTooltip="Tooltip on disabled button" + [disabled]="true" + ></button> + </div> + `, + }), +}; diff --git a/libs/components/src/tw-theme-preflight.css b/libs/components/src/tw-theme-preflight.css index e5f35885993..372c80e0881 100644 --- a/libs/components/src/tw-theme-preflight.css +++ b/libs/components/src/tw-theme-preflight.css @@ -54,6 +54,14 @@ display: none !important; } + dl { + @apply tw-mb-4; + } + + dt { + @apply tw-font-bold; + } + hr { border-color: rgba(0, 0, 0, 0.1); } diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index ec29bc522eb..1e0a6f438f0 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -5,6 +5,7 @@ @import "./popover/popover.component.css"; @import "./toast/toast.tokens.css"; @import "./toast/toastr.css"; +@import "./tooltip/tooltip.component.css"; @import "./search/search.component.css"; @tailwind base; diff --git a/libs/importer/src/components/chrome/import-chrome.component.ts b/libs/importer/src/components/chrome/import-chrome.component.ts index 9e9f985c5a5..bf2a37203e1 100644 --- a/libs/importer/src/components/chrome/import-chrome.component.ts +++ b/libs/importer/src/components/chrome/import-chrome.component.ts @@ -114,7 +114,11 @@ export class ImportChromeComponent implements OnInit, OnDestroy { this.formGroup.controls.profile.value, ); if (logins.length === 0) { - throw "nothing to import"; + return { + errors: { + message: this.i18nService.t("importNothingError"), + }, + }; } const chromeLogins: ChromeLogin[] = []; for (const l of logins) { diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index 9f1247b52da..bca7d15f087 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -49,7 +49,7 @@ *ngFor="let c of collections$ | async" [value]="c" [label]="c.name" - icon="bwi-collection-shared" + [icon]="c.type === DefaultCollectionType ? 'bwi-user' : 'bwi-collection-shared'" /> </ng-container> </bit-select> diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 54c3fc1d408..d98cd817147 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -29,7 +29,11 @@ import { combineLatestWith, filter, map, switchMap, takeUntil } from "rxjs/opera // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { + CollectionService, + CollectionTypes, + CollectionView, +} from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { getOrganizationById, @@ -103,6 +107,8 @@ import { ImportLastPassComponent } from "./lastpass"; providers: ImporterProviders, }) export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { + DefaultCollectionType = CollectionTypes.DefaultUserCollection; + featuredImportOptions: ImportOption[]; importOptions: ImportOption[]; format: ImportType = null; diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts index 19a8a4828e1..f8acb5e0643 100644 --- a/libs/importer/src/importers/base-importer.ts +++ b/libs/importer/src/importers/base-importer.ts @@ -193,7 +193,6 @@ export abstract class BaseImporter { if (this.isNullOrWhitespace(loginUri.uri)) { return null; } - loginUri.match = null; return [loginUri]; } @@ -205,7 +204,6 @@ export abstract class BaseImporter { if (this.isNullOrWhitespace(loginUri.uri)) { return; } - loginUri.match = null; returnArr.push(loginUri); }); return returnArr.length === 0 ? null : returnArr; @@ -236,7 +234,7 @@ export abstract class BaseImporter { return hostname.startsWith("www.") ? hostname.replace("www.", "") : hostname; } - protected isNullOrWhitespace(str: string): boolean { + protected isNullOrWhitespace(str: string | undefined | null): boolean { return Utils.isNullOrWhitespace(str); } diff --git a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts index 70c783df52a..14a16211deb 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts @@ -64,12 +64,13 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { private async parseEncrypted( results: BitwardenEncryptedIndividualJsonExport | BitwardenEncryptedOrgJsonExport, ) { + const account = await firstValueFrom(this.accountService.activeAccount$); + if (results.encKeyValidation_DO_NOT_EDIT != null) { - let keyForDecryption: SymmetricCryptoKey = await this.keyService.getOrgKey( - this.organizationId, - ); + const orgKeys = await firstValueFrom(this.keyService.orgKeys$(account.id)); + let keyForDecryption: SymmetricCryptoKey = orgKeys?.[this.organizationId]; if (keyForDecryption == null) { - keyForDecryption = await this.keyService.getUserKey(); + keyForDecryption = await firstValueFrom(this.keyService.userKey$(account.id)); } const encKeyValidation = new EncString(results.encKeyValidation_DO_NOT_EDIT); try { @@ -113,10 +114,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { }); } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const view = await this.cipherService.decrypt(cipher, activeUserId); + const view = await this.cipherService.decrypt(cipher, account.id); this.cleanupCipher(view); this.result.ciphers.push(view); } diff --git a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts index 7812cce2c05..dfdcef51735 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.spec.ts @@ -1,12 +1,17 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { emptyGuid, OrganizationId } from "@bitwarden/common/types/guid"; +import { OrgKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { newGuid } from "@bitwarden/guid"; import { KdfType, KeyService } from "@bitwarden/key-management"; +import { UserId } from "@bitwarden/user-core"; import { emptyAccountEncrypted } from "../spec-data/bitwarden-json/account-encrypted.json"; import { emptyUnencryptedExport } from "../spec-data/bitwarden-json/unencrypted.json"; @@ -35,6 +40,36 @@ describe("BitwardenPasswordProtectedImporter", () => { pinService = mock<PinServiceAbstraction>(); accountService = mock<AccountService>(); + accountService.activeAccount$ = of({ + id: newGuid() as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); + + const mockOrgId = emptyGuid as OrganizationId; + /* + The key values below are never read, empty objects are cast as types for compilation type checking only. + Tests specific to key contents are in key-service.spec.ts + */ + const mockOrgKey = {} as unknown as OrgKey; + const mockUserKey = {} as unknown as UserKey; + + keyService.orgKeys$.mockImplementation(() => + of({ [mockOrgId]: mockOrgKey } as Record<OrganizationId, OrgKey>), + ); + keyService.userKey$.mockImplementation(() => of(mockUserKey)); + (keyService as any).activeUserOrgKeys$ = of({ + [mockOrgId]: mockOrgKey, + } as Record<OrganizationId, OrgKey>); + + /* + Crypto isn’t under test here; keys are just placeholders. + Decryption methods are stubbed to always return empty CipherView or string allowing OK import flow. + */ + cipherService.decrypt.mockResolvedValue({} as any); + encryptService.decryptString.mockResolvedValue("ok"); + importer = new BitwardenPasswordProtectedImporter( keyService, encryptService, @@ -62,6 +97,24 @@ describe("BitwardenPasswordProtectedImporter", () => { jest.spyOn(BitwardenJsonImporter.prototype, "parse"); }); + beforeEach(() => { + accountService.activeAccount$ = of({ + id: newGuid() as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); + importer = new BitwardenPasswordProtectedImporter( + keyService, + encryptService, + i18nService, + cipherService, + pinService, + accountService, + promptForPassword_callback, + ); + }); + it("Should call BitwardenJsonImporter", async () => { expect((await importer.parse(emptyAccountEncrypted)).success).toEqual(true); expect(BitwardenJsonImporter.prototype.parse).toHaveBeenCalledWith(emptyAccountEncrypted); diff --git a/libs/importer/src/importers/chrome-csv-importer.spec.ts b/libs/importer/src/importers/chrome-csv-importer.spec.ts index a7a29094707..df60a6f2647 100644 --- a/libs/importer/src/importers/chrome-csv-importer.spec.ts +++ b/libs/importer/src/importers/chrome-csv-importer.spec.ts @@ -11,9 +11,6 @@ const CipherData = [ title: "should parse app name", csv: androidData, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "com.xyz.example.app.android", login: Object.assign(new LoginView(), { username: "username@example.com", @@ -24,7 +21,6 @@ const CipherData = [ }), ], }), - notes: null, type: 1, }), }, @@ -32,9 +28,6 @@ const CipherData = [ title: "should parse password", csv: simplePasswordData, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "www.example.com", login: Object.assign(new LoginView(), { username: "username@example.com", @@ -45,7 +38,6 @@ const CipherData = [ }), ], }), - notes: null, type: 1, }), }, @@ -54,6 +46,7 @@ const CipherData = [ describe("Chrome CSV Importer", () => { CipherData.forEach((data) => { it(data.title, async () => { + jest.useFakeTimers().setSystemTime(data.expected.creationDate); const importer = new ChromeCsvImporter(); const result = await importer.parse(data.csv); expect(result != null).toBe(true); diff --git a/libs/importer/src/importers/dashlane/dashlane-csv-importer.spec.ts b/libs/importer/src/importers/dashlane/dashlane-csv-importer.spec.ts index b8d84a9378a..2dedcec6b2a 100644 --- a/libs/importer/src/importers/dashlane/dashlane-csv-importer.spec.ts +++ b/libs/importer/src/importers/dashlane/dashlane-csv-importer.spec.ts @@ -59,12 +59,12 @@ describe("Dashlane CSV Importer", () => { const cipher = result.ciphers.shift(); expect(cipher.type).toBe(CipherType.Card); expect(cipher.name).toBe("John's savings account"); - expect(cipher.card.brand).toBeNull(); + expect(cipher.card.brand).toBeUndefined(); expect(cipher.card.cardholderName).toBe("John Doe"); expect(cipher.card.number).toBe("accountNumber"); - expect(cipher.card.code).toBeNull(); - expect(cipher.card.expMonth).toBeNull(); - expect(cipher.card.expYear).toBeNull(); + expect(cipher.card.code).toBeUndefined(); + expect(cipher.card.expMonth).toBeUndefined(); + expect(cipher.card.expYear).toBeUndefined(); expect(cipher.fields.length).toBe(4); @@ -112,7 +112,7 @@ describe("Dashlane CSV Importer", () => { expect(cipher.name).toBe("John Doe card"); expect(cipher.identity.fullName).toBe("John Doe"); expect(cipher.identity.firstName).toBe("John"); - expect(cipher.identity.middleName).toBeNull(); + expect(cipher.identity.middleName).toBeUndefined(); expect(cipher.identity.lastName).toBe("Doe"); expect(cipher.identity.licenseNumber).toBe("123123123"); @@ -133,7 +133,7 @@ describe("Dashlane CSV Importer", () => { expect(cipher2.name).toBe("John Doe passport"); expect(cipher2.identity.fullName).toBe("John Doe"); expect(cipher2.identity.firstName).toBe("John"); - expect(cipher2.identity.middleName).toBeNull(); + expect(cipher2.identity.middleName).toBeUndefined(); expect(cipher2.identity.lastName).toBe("Doe"); expect(cipher2.identity.passportNumber).toBe("123123123"); @@ -154,7 +154,7 @@ describe("Dashlane CSV Importer", () => { expect(cipher3.name).toBe("John Doe license"); expect(cipher3.identity.fullName).toBe("John Doe"); expect(cipher3.identity.firstName).toBe("John"); - expect(cipher3.identity.middleName).toBeNull(); + expect(cipher3.identity.middleName).toBeUndefined(); expect(cipher3.identity.lastName).toBe("Doe"); expect(cipher3.identity.licenseNumber).toBe("1234556"); expect(cipher3.identity.state).toBe("DC"); @@ -173,7 +173,7 @@ describe("Dashlane CSV Importer", () => { expect(cipher4.name).toBe("John Doe social_security"); expect(cipher4.identity.fullName).toBe("John Doe"); expect(cipher4.identity.firstName).toBe("John"); - expect(cipher4.identity.middleName).toBeNull(); + expect(cipher4.identity.middleName).toBeUndefined(); expect(cipher4.identity.lastName).toBe("Doe"); expect(cipher4.identity.ssn).toBe("123123123"); diff --git a/libs/importer/src/importers/firefox-csv-importer.spec.ts b/libs/importer/src/importers/firefox-csv-importer.spec.ts index 78bca0599b5..59d2aa9e7a4 100644 --- a/libs/importer/src/importers/firefox-csv-importer.spec.ts +++ b/libs/importer/src/importers/firefox-csv-importer.spec.ts @@ -11,9 +11,6 @@ const CipherData = [ title: "should parse password", csv: simplePasswordData, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "example.com", login: Object.assign(new LoginView(), { username: "foo", @@ -24,7 +21,6 @@ const CipherData = [ }), ], }), - notes: null, type: 1, }), }, @@ -32,9 +28,6 @@ const CipherData = [ title: 'should skip "chrome://FirefoxAccounts"', csv: firefoxAccountsData, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "example.com", login: Object.assign(new LoginView(), { username: "foo", @@ -45,7 +38,6 @@ const CipherData = [ }), ], }), - notes: null, type: 1, }), }, @@ -54,6 +46,7 @@ const CipherData = [ describe("Firefox CSV Importer", () => { CipherData.forEach((data) => { it(data.title, async () => { + jest.useFakeTimers().setSystemTime(data.expected.creationDate); const importer = new FirefoxCsvImporter(); const result = await importer.parse(data.csv); expect(result != null).toBe(true); diff --git a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts index b326bc5d351..dcaacffc05e 100644 --- a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts @@ -51,10 +51,10 @@ describe("Keeper CSV Importer", () => { expect(result != null).toBe(true); const cipher = result.ciphers.shift(); - expect(cipher.login.totp).toBeNull(); + expect(cipher.login.totp).toBeUndefined(); const cipher2 = result.ciphers.shift(); - expect(cipher2.login.totp).toBeNull(); + expect(cipher2.login.totp).toBeUndefined(); const cipher3 = result.ciphers.shift(); expect(cipher3.login.totp).toEqual( diff --git a/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts index 1141897a044..a9d42369b1e 100644 --- a/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts @@ -51,7 +51,7 @@ describe("Keeper Json Importer", () => { expect(result != null).toBe(true); const cipher = result.ciphers.shift(); - expect(cipher.login.totp).toBeNull(); + expect(cipher.login.totp).toBeUndefined(); // 2nd Cipher const cipher2 = result.ciphers.shift(); diff --git a/libs/importer/src/importers/lastpass/lastpass-csv-importer.spec.ts b/libs/importer/src/importers/lastpass/lastpass-csv-importer.spec.ts index cabd246fa7e..6515e3959b0 100644 --- a/libs/importer/src/importers/lastpass/lastpass-csv-importer.spec.ts +++ b/libs/importer/src/importers/lastpass/lastpass-csv-importer.spec.ts @@ -37,9 +37,6 @@ Expiration Date:June,2020 Notes:some text ",Credit-card,,0`, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "Credit-card", notes: "some text\n", type: 3, @@ -71,11 +68,7 @@ Start Date:, Expiration Date:, Notes:",empty,,0`, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "empty", - notes: null, type: 3, card: { expMonth: undefined, @@ -101,11 +94,7 @@ Start Date:, Expiration Date:January, Notes:",noyear,,0`, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "noyear", - notes: null, type: 3, card: { cardholderName: "John Doe", @@ -139,11 +128,7 @@ Start Date:, Expiration Date:,2020 Notes:",nomonth,,0`, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "nomonth", - notes: null, type: 3, card: { cardholderName: "John Doe", @@ -171,6 +156,7 @@ Notes:",nomonth,,0`, describe("Lastpass CSV Importer", () => { CipherData.forEach((data) => { it(data.title, async () => { + jest.useFakeTimers().setSystemTime(data.expected.creationDate); const importer = new LastPassCsvImporter(); const result = await importer.parse(data.csv); expect(result != null).toBe(true); diff --git a/libs/importer/src/importers/myki-csv-importer.spec.ts b/libs/importer/src/importers/myki-csv-importer.spec.ts index 6f804523ef0..a77e85d134a 100644 --- a/libs/importer/src/importers/myki-csv-importer.spec.ts +++ b/libs/importer/src/importers/myki-csv-importer.spec.ts @@ -468,8 +468,8 @@ describe("Myki CSV Importer", () => { const cipher = result.ciphers.shift(); expect(cipher.name).toEqual("2FA nickname"); - expect(cipher.login.username).toBeNull(); - expect(cipher.login.password).toBeNull(); + expect(cipher.login.username).toBeUndefined(); + expect(cipher.login.password).toBeUndefined(); expect(cipher.login.totp).toBe("someTOTPSeed"); expect(cipher.notes).toEqual("Additional information field content."); diff --git a/libs/importer/src/importers/nordpass-csv-importer.spec.ts b/libs/importer/src/importers/nordpass-csv-importer.spec.ts index e633310e6ee..f04272de012 100644 --- a/libs/importer/src/importers/nordpass-csv-importer.spec.ts +++ b/libs/importer/src/importers/nordpass-csv-importer.spec.ts @@ -17,8 +17,8 @@ const namesTestData = [ fullName: "MyFirstName", expected: Object.assign(new IdentityView(), { firstName: "MyFirstName", - middleName: null, - lastName: null, + middleName: undefined, + lastName: undefined, }), }, { @@ -26,7 +26,7 @@ const namesTestData = [ fullName: "MyFirstName MyLastName", expected: Object.assign(new IdentityView(), { firstName: "MyFirstName", - middleName: null, + middleName: undefined, lastName: "MyLastName", }), }, diff --git a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts index 1ca12a9ce69..4ec20ba2a87 100644 --- a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts @@ -393,7 +393,7 @@ describe("1Password 1Pux Importer", () => { const identity = cipher.identity; expect(identity.firstName).toEqual("Michael"); - expect(identity.middleName).toBeNull(); + expect(identity.middleName).toBeUndefined(); expect(identity.lastName).toEqual("Scarn"); expect(identity.address1).toEqual("2120 Mifflin Rd."); expect(identity.state).toEqual("Pennsylvania"); @@ -423,7 +423,7 @@ describe("1Password 1Pux Importer", () => { const identity = cipher.identity; expect(identity.firstName).toEqual("Cash"); - expect(identity.middleName).toBeNull(); + expect(identity.middleName).toBeUndefined(); expect(identity.lastName).toEqual("Bandit"); expect(identity.state).toEqual("Washington"); expect(identity.country).toEqual("United States of America"); @@ -447,7 +447,7 @@ describe("1Password 1Pux Importer", () => { const identity = cipher.identity; expect(identity.firstName).toEqual("George"); - expect(identity.middleName).toBeNull(); + expect(identity.middleName).toBeUndefined(); expect(identity.lastName).toEqual("Engels"); expect(identity.company).toEqual("National Public Library"); expect(identity.phone).toEqual("9995555555"); @@ -472,7 +472,7 @@ describe("1Password 1Pux Importer", () => { const identity = cipher.identity; expect(identity.firstName).toEqual("David"); - expect(identity.middleName).toBeNull(); + expect(identity.middleName).toBeUndefined(); expect(identity.lastName).toEqual("Global"); expect(identity.passportNumber).toEqual("76436847"); @@ -499,7 +499,7 @@ describe("1Password 1Pux Importer", () => { const identity = cipher.identity; expect(identity.firstName).toEqual("Chef"); - expect(identity.middleName).toBeNull(); + expect(identity.middleName).toBeUndefined(); expect(identity.lastName).toEqual("Coldroom"); expect(identity.company).toEqual("Super Cool Store Co."); @@ -523,7 +523,7 @@ describe("1Password 1Pux Importer", () => { const identity = cipher.identity; expect(identity.firstName).toEqual("Jack"); - expect(identity.middleName).toBeNull(); + expect(identity.middleName).toBeUndefined(); expect(identity.lastName).toEqual("Judd"); expect(identity.ssn).toEqual("131-216-1900"); }); @@ -682,12 +682,12 @@ describe("1Password 1Pux Importer", () => { expect(folders[3].name).toBe("Education"); expect(folders[4].name).toBe("Starter Kit"); - // Check that ciphers have a folder assigned to them - expect(result.ciphers.filter((c) => c.folderId === folders[0].id).length).toBeGreaterThan(0); - expect(result.ciphers.filter((c) => c.folderId === folders[1].id).length).toBeGreaterThan(0); - expect(result.ciphers.filter((c) => c.folderId === folders[2].id).length).toBeGreaterThan(0); - expect(result.ciphers.filter((c) => c.folderId === folders[3].id).length).toBeGreaterThan(0); - expect(result.ciphers.filter((c) => c.folderId === folders[4].id).length).toBeGreaterThan(0); + // Check that folder/cipher relationships + expect(result.folderRelationships.filter(([_, f]) => f == 0).length).toBeGreaterThan(0); + expect(result.folderRelationships.filter(([_, f]) => f == 1).length).toBeGreaterThan(0); + expect(result.folderRelationships.filter(([_, f]) => f == 2).length).toBeGreaterThan(0); + expect(result.folderRelationships.filter(([_, f]) => f == 3).length).toBeGreaterThan(0); + expect(result.folderRelationships.filter(([_, f]) => f == 4).length).toBeGreaterThan(0); }); it("should create collections if part of an organization", async () => { diff --git a/libs/importer/src/importers/safari-csv-importer.spec.ts b/libs/importer/src/importers/safari-csv-importer.spec.ts index 4ca8df23f34..c55117226f9 100644 --- a/libs/importer/src/importers/safari-csv-importer.spec.ts +++ b/libs/importer/src/importers/safari-csv-importer.spec.ts @@ -11,9 +11,6 @@ const CipherData = [ title: "should parse URLs in new CSV format", csv: simplePasswordData, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "example.com (example_user)", login: Object.assign(new LoginView(), { username: "example_user", @@ -33,9 +30,6 @@ const CipherData = [ title: "should parse URLs in old CSV format", csv: oldSimplePasswordData, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "example.com (example_user)", login: Object.assign(new LoginView(), { username: "example_user", @@ -45,6 +39,7 @@ const CipherData = [ uri: "https://example.com", }), ], + totp: null, }), type: 1, }), @@ -54,6 +49,7 @@ const CipherData = [ describe("Safari CSV Importer", () => { CipherData.forEach((data) => { it(data.title, async () => { + jest.useFakeTimers().setSystemTime(data.expected.creationDate); const importer = new SafariCsvImporter(); const result = await importer.parse(data.csv); expect(result != null).toBe(true); diff --git a/libs/importer/src/importers/zohovault-csv-importer.spec.ts b/libs/importer/src/importers/zohovault-csv-importer.spec.ts index d3904fb521a..c82e3e5dcf1 100644 --- a/libs/importer/src/importers/zohovault-csv-importer.spec.ts +++ b/libs/importer/src/importers/zohovault-csv-importer.spec.ts @@ -11,9 +11,6 @@ const CipherData = [ title: "should parse Zoho Vault CSV format", csv: samplezohovaultcsvdata, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, name: "XYZ Test", login: Object.assign(new LoginView(), { username: "email@domain.de", @@ -41,6 +38,7 @@ describe("Zoho Vault CSV Importer", () => { CipherData.forEach((data) => { it(data.title, async () => { + jest.useFakeTimers().setSystemTime(data.expected.creationDate); const importer = new ZohoVaultCsvImporter(); const result = await importer.parse(data.csv); expect(result != null).toBe(true); diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index d8f828b1ce2..09580479382 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -145,7 +145,8 @@ export class ImportService implements ImportServiceAbstraction { map(([type, enabled]) => { let loaders = availableLoaders(type, client); - let isUnsupported = false; + // Mac App Store is currently disabled due to sandboxing. + let isUnsupported = this.system.environment.isMacAppStore(); if (enabled && type === "bravecsv") { try { diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts index 9968908a06f..1c09b071e99 100644 --- a/libs/state/src/core/state-definitions.ts +++ b/libs/state/src/core/state-definitions.ts @@ -72,6 +72,7 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { web: "disk-local", }); export const TOKEN_MEMORY = new StateDefinition("token", "memory"); +export const SEND_ACCESS_DISK = new StateDefinition("sendAccess", "disk"); export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory"); export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); export const ORGANIZATION_INVITE_DISK = new StateDefinition("organizationInvite", "disk"); @@ -110,9 +111,6 @@ export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanne export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", { web: "disk-local", }); -export const BADGE_MEMORY = new StateDefinition("badge", "memory", { - browser: "memory-large-object", -}); export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk"); export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk"); export const CONFIG_DISK = new StateDefinition("config", "disk", { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index 3884dde4b18..53952938aa8 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -15,6 +15,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -22,6 +23,7 @@ import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { newGuid } from "@bitwarden/guid"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { @@ -112,7 +114,7 @@ export class OrganizationVaultExportService type: "text/plain", data: onlyManagedCollections ? await this.getEncryptedManagedExport(userId, organizationId) - : await this.getOrganizationEncryptedExport(organizationId), + : await this.getOrganizationEncryptedExport(userId, organizationId), fileName: ExportHelper.getFileName("org", "encrypted_json"), } as ExportedVaultAsString; } @@ -184,7 +186,10 @@ export class OrganizationVaultExportService return this.buildJsonExport(decCollections, decCiphers); } - private async getOrganizationEncryptedExport(organizationId: OrganizationId): Promise<string> { + private async getOrganizationEncryptedExport( + userId: UserId, + organizationId: OrganizationId, + ): Promise<string> { const collections: Collection[] = []; const ciphers: Cipher[] = []; @@ -215,7 +220,7 @@ export class OrganizationVaultExportService } }); } - return this.BuildEncryptedExport(organizationId, collections, ciphers); + return this.BuildEncryptedExport(userId, organizationId, collections, ciphers); } private async getDecryptedManagedExport( @@ -295,16 +300,21 @@ export class OrganizationVaultExportService !this.restrictedItemTypesService.isCipherRestricted(f, restrictions), ); - return this.BuildEncryptedExport(organizationId, encCollections, encCiphers); + return this.BuildEncryptedExport(activeUserId, organizationId, encCollections, encCiphers); } private async BuildEncryptedExport( + activeUserId: UserId, organizationId: OrganizationId, collections: Collection[], ciphers: Cipher[], ): Promise<string> { - const orgKey = await this.keyService.getOrgKey(organizationId); - const encKeyValidation = await this.encryptService.encryptString(Utils.newGuid(), orgKey); + const orgKeys = await firstValueFrom(this.keyService.orgKeys$(activeUserId)); + const keyForEncryption: SymmetricCryptoKey = orgKeys?.[organizationId]; + if (keyForEncryption == null) { + throw new Error("No encryption key found for organization"); + } + const encKeyValidation = await this.encryptService.encryptString(newGuid(), keyForEncryption); const jsonDoc: BitwardenEncryptedOrgJsonExport = { encrypted: true, diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts index 2b03234c5e2..a85048c23fa 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts @@ -5,12 +5,10 @@ import { Component, effect, input } from "@angular/core"; import { firstValueFrom, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - getOrganizationById, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +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 { getById } from "@bitwarden/common/platform/misc/rxjs-operators"; import { CalloutModule } from "@bitwarden/components"; @Component({ @@ -30,6 +28,8 @@ export class ExportScopeCalloutComponent { readonly organizationId = input<string>(); /* Optional export format, determines which individual export description to display */ readonly exportFormat = input<string>(); + /* The description key to use for organizational exports */ + readonly orgExportDescription = input<string>(); constructor( protected organizationService: OrganizationService, @@ -37,35 +37,45 @@ export class ExportScopeCalloutComponent { ) { effect(async () => { this.show = false; - await this.getScopeMessage(this.organizationId(), this.exportFormat()); + await this.getScopeMessage( + this.organizationId(), + this.exportFormat(), + this.orgExportDescription(), + ); this.show = true; }); } - private async getScopeMessage(organizationId: string, exportFormat: string): Promise<void> { + private async getScopeMessage( + organizationId: string, + exportFormat: string, + orgExportDescription: string, + ): Promise<void> { const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - this.scopeConfig = - organizationId != null - ? { - title: "exportingOrganizationVaultTitle", - description: "exportingOrganizationVaultDesc", - scopeIdentifier: ( - await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(organizationId)), - ) - ).name, - } - : { - title: "exportingPersonalVaultTitle", - description: - exportFormat == "zip" - ? "exportingIndividualVaultWithAttachmentsDescription" - : "exportingIndividualVaultDescription", - scopeIdentifier: await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.email)), - ), - }; + + if (organizationId != null) { + // exporting from organizational vault + const org = await firstValueFrom( + this.organizationService.organizations$(userId).pipe(getById(organizationId)), + ); + + this.scopeConfig = { + title: "exportingOrganizationVaultTitle", + description: orgExportDescription, + scopeIdentifier: org?.name ?? "", + }; + } else { + this.scopeConfig = { + // exporting from individual vault + title: "exportingPersonalVaultTitle", + description: + exportFormat === "zip" + ? "exportingIndividualVaultWithAttachmentsDescription" + : "exportingIndividualVaultDescription", + scopeIdentifier: + (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.email)))) ?? + "", + }; + } } } diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html index b33b01d3b13..c638e5d7dde 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html @@ -8,6 +8,7 @@ <tools-export-scope-callout [organizationId]="organizationId" [exportFormat]="format" + [orgExportDescription]="orgExportDescription" ></tools-export-scope-callout> <form [formGroup]="exportForm" [bitSubmit]="submit" id="export_form_exportForm"> @@ -19,10 +20,10 @@ [label]="'myVault' | i18n" value="myVault" icon="bwi-user" - *ngIf="!(organizationDataOwnershipPolicy$ | async)" + *ngIf="!(organizationDataOwnershipPolicyAppliesToUser$ | async)" /> <bit-option - *ngFor="let o of organizations$ | async" + *ngFor="let o of organizations" [value]="o.id" [label]="o.name" icon="bwi-business" diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index f2caf4fe3f4..567480ac1bd 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -10,14 +10,20 @@ import { OnInit, Output, ViewChild, + Optional, } from "@angular/core"; import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; import { + BehaviorSubject, combineLatest, firstValueFrom, + from, map, merge, Observable, + of, + shareReplay, startWith, Subject, switchMap, @@ -36,10 +42,13 @@ 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 { EventType } from "@bitwarden/common/enums"; +import { ClientType, EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { getById } from "@bitwarden/common/platform/misc"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { pin } from "@bitwarden/common/tools/rx"; @@ -84,11 +93,9 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component"; ], }) export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { - private _organizationId: OrganizationId | undefined; - - get organizationId(): OrganizationId | undefined { - return this._organizationId; - } + private _organizationId$ = new BehaviorSubject<OrganizationId | undefined>(undefined); + private createDefaultLocationFlagEnabled$: Observable<boolean>; + private _showExcludeMyItems = false; /** * Enables the hosting control to pass in an organizationId @@ -96,29 +103,57 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { */ @Input() set organizationId(value: OrganizationId | string | undefined) { if (Utils.isNullOrEmpty(value)) { - this._organizationId = undefined; + this._organizationId$.next(undefined); return; } if (!isId<OrganizationId>(value)) { - this._organizationId = undefined; + this._organizationId$.next(undefined); return; } - this._organizationId = value; + this._organizationId$.next(value); getUserId(this.accountService.activeAccount$) .pipe( - switchMap((userId) => - this.organizationService.organizations$(userId).pipe(getById(this._organizationId)), - ), + switchMap((userId) => this.organizationService.organizations$(userId).pipe(getById(value))), ) .pipe(takeUntil(this.destroy$)) .subscribe((organization) => { - this._organizationId = organization?.id; + this._organizationId$.next(organization?.id); }); } + get organizationId(): OrganizationId | undefined { + return this._organizationId$.value; + } + + get showExcludeMyItems(): boolean { + return this._showExcludeMyItems; + } + + get orgExportDescription(): string { + if (!this._showExcludeMyItems) { + return "exportingOrganizationVaultDesc"; + } + return this.isAdminConsoleContext + ? "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc" + : "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc"; + } + + private get isAdminConsoleContext(): boolean { + const isWeb = this.platformUtilsService.getClientType?.() === ClientType.Web; + if (!isWeb || !this.router) { + return false; + } + try { + const url = this.router.url ?? ""; + return url.includes("/organizations/"); + } catch { + return false; + } + } + /** * The hosting control also needs a bitSubmitDirective (on the Submit button) which calls this components {@link submit}-method. * This components formState (loading/disabled) is emitted back up to the hosting component so for example the Submit button can be enabled/disabled and show loading state. @@ -143,7 +178,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { /** * Emits when the creation and download of the export-file have succeeded * - Emits an undefined when exporting from an individual vault - * - Emits the organizationId when exporting from an organizationl vault + * - Emits the organizationId when exporting from an organizational vault * */ @Output() onSuccessfulExport = new EventEmitter<OrganizationId | undefined>(); @@ -162,7 +197,10 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { } disablePersonalVaultExportPolicy$: Observable<boolean>; - organizationDataOwnershipPolicy$: Observable<boolean>; + // detects if policy is enabled and applies to the user, admins are exempted + organizationDataOwnershipPolicyAppliesToUser$: Observable<boolean>; + // detects if policy is enabled regardless of admin exemption + organizationDataOwnershipPolicyEnabledForOrg$: Observable<boolean>; exportForm = this.formBuilder.group({ vaultSelector: [ @@ -203,14 +241,46 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { protected organizationService: OrganizationService, private accountService: AccountService, private collectionService: CollectionService, + private configService: ConfigService, + private platformUtilsService: PlatformUtilsService, + @Optional() private router?: Router, ) {} async ngOnInit() { - // Setup subscription to emit when this form is enabled/disabled + this.observeFeatureFlags(); + this.observeFormState(); + this.observePolicyStatus(); + this.observeFormSelections(); + + // order is important below this line + this.observeMyItemsExclusionCriteria(); + this.observeValidatorAdjustments(); + this.setupPasswordGeneration(); + + if (this.organizationId) { + // organization vault export + this.initOrganizationOnly(); + return; + } + + // individual vault export + this.initIndividual(); + this.setupPolicyBasedFormState(); + } + + private observeFeatureFlags(): void { + this.createDefaultLocationFlagEnabled$ = from( + this.configService.getFeatureFlag(FeatureFlag.CreateDefaultLocation), + ).pipe(shareReplay({ bufferSize: 1, refCount: true })); + } + + private observeFormState(): void { this.exportForm.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => { this.formDisabled.emit(c === "DISABLED"); }); + } + private observePolicyStatus(): void { this.disablePersonalVaultExportPolicy$ = this.accountService.activeAccount$.pipe( getUserId, switchMap((userId) => @@ -218,13 +288,42 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { ), ); - this.organizationDataOwnershipPolicy$ = this.accountService.activeAccount$.pipe( + // when true, html template will hide "My Vault" option in vault selector drop down + this.organizationDataOwnershipPolicyAppliesToUser$ = this.accountService.activeAccount$.pipe( getUserId, switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId), ), ); + /* + Determines how organization exports are described in the callout. + Admins are exempted from organization data ownership policy, + and so this needs to determine if the policy is enabled for the org, not if it applies to the user. + */ + this.organizationDataOwnershipPolicyEnabledForOrg$ = combineLatest([ + this.accountService.activeAccount$.pipe(getUserId), + this._organizationId$, + ]).pipe( + switchMap(([userId, organizationId]) => { + if (!organizationId || !userId) { + return of(false); + } + return this.policyService.policies$(userId).pipe( + map((policies) => { + const policy = policies?.find( + (p) => + p.type === PolicyType.OrganizationDataOwnership && + p.organizationId === organizationId, + ); + return policy?.enabled ?? false; + }), + ); + }), + ); + } + + private observeFormSelections(): void { this.exportForm.controls.vaultSelector.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe((value) => { @@ -236,15 +335,50 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { this.formatOptions.push({ name: ".zip (with attachments)", value: "zip" }); } }); + } + /** + * Determine value of showExcludeMyItems. Returns true when: + * CreateDefaultLocation feature flag is on + * AND organizationDataOwnershipPolicy is enabled for the selected organization + * AND a valid OrganizationId is present (not exporting from individual vault) + */ + private observeMyItemsExclusionCriteria(): void { + combineLatest({ + createDefaultLocationFlagEnabled: this.createDefaultLocationFlagEnabled$, + organizationDataOwnershipPolicyEnabledForOrg: + this.organizationDataOwnershipPolicyEnabledForOrg$, + organizationId: this._organizationId$, + }) + .pipe(takeUntil(this.destroy$)) + .subscribe( + ({ + createDefaultLocationFlagEnabled, + organizationDataOwnershipPolicyEnabledForOrg, + organizationId, + }) => { + if (!createDefaultLocationFlagEnabled || !organizationId) { + this._showExcludeMyItems = false; + return; + } + + this._showExcludeMyItems = organizationDataOwnershipPolicyEnabledForOrg; + }, + ); + } + + // Setup validator adjustments based on format and encryption type changes + private observeValidatorAdjustments(): void { merge( this.exportForm.get("format").valueChanges, this.exportForm.get("fileEncryptionType").valueChanges, ) .pipe(startWith(0), takeUntil(this.destroy$)) .subscribe(() => this.adjustValidators()); + } - // Wire up the password generation for the password-protected export + // Wire up the password generation for password-protected exports + private setupPasswordGeneration(): void { const account$ = this.accountService.activeAccount$.pipe( pin({ name() { @@ -255,6 +389,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { }, }), ); + this.generatorService .generate$({ on$: this.onGenerate$, account$ }) .pipe(takeUntil(this.destroy$)) @@ -264,23 +399,29 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { confirmFilePassword: generated.credential, }); }); + } - if (this.organizationId) { - this.organizations$ = this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => - this.organizationService - .memberOrganizations$(userId) - .pipe(map((orgs) => orgs.filter((org) => org.id == this.organizationId))), - ), - ); - this.exportForm.controls.vaultSelector.patchValue(this.organizationId); - this.exportForm.controls.vaultSelector.disable(); + /* + Initialize component for organization only export + Hides "My Vault" option by returning immediately + */ + private initOrganizationOnly(): void { + this.organizations$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.organizationService + .memberOrganizations$(userId) + .pipe(map((orgs) => orgs.filter((org) => org.id == this.organizationId))), + ), + ); + this.exportForm.controls.vaultSelector.patchValue(this.organizationId); + this.exportForm.controls.vaultSelector.disable(); - this.onlyManagedCollections = false; - return; - } + this.onlyManagedCollections = false; + } + // Initialize component to support individual and organizational exports + private initIndividual(): void { this.organizations$ = this.accountService.activeAccount$ .pipe( getUserId, @@ -296,18 +437,18 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { const managedCollectionsOrgIds = new Set( collections.filter((c) => c.manage).map((c) => c.organizationId), ); - // Filter organizations that exist in managedCollectionsOrgIds const filteredOrgs = memberOrganizations.filter((org) => managedCollectionsOrgIds.has(org.id), ); - // Sort the filtered organizations based on the name return filteredOrgs.sort(Utils.getSortFunction(this.i18nService, "name")); }), ); + } + private setupPolicyBasedFormState(): void { combineLatest([ this.disablePersonalVaultExportPolicy$, - this.organizationDataOwnershipPolicy$, + this.organizationDataOwnershipPolicyAppliesToUser$, this.organizations$, ]) .pipe( diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts index 16588f92807..a9a327b90c0 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts @@ -28,7 +28,7 @@ describe("AdditionalOptionsSectionComponent", () => { let passwordRepromptService: MockProxy<PasswordRepromptService>; let passwordRepromptEnabled$: BehaviorSubject<boolean>; - const getInitialCipherView = jest.fn(() => null); + const getInitialCipherView = jest.fn((): any => null); const formStatusChange$ = new BehaviorSubject<"enabled" | "disabled">("enabled"); beforeEach(async () => { @@ -86,7 +86,10 @@ describe("AdditionalOptionsSectionComponent", () => { expect(cipherFormProvider.patchCipher).toHaveBeenCalled(); const patchFn = cipherFormProvider.patchCipher.mock.lastCall[0]; - const updated = patchFn(new CipherView()); + const newCipher = new CipherView(); + newCipher.creationDate = newCipher.revisionDate = expectedCipher.creationDate; + + const updated = patchFn(newCipher); expect(updated).toEqual(expectedCipher); }); diff --git a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts index 1552ef9f30e..60002ca5924 100644 --- a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts @@ -66,7 +66,7 @@ export class DeleteAttachmentComponent { await this.cipherService.deleteAttachmentWithServer( this.cipherId, - this.attachment.id, + this.attachment.id!, activeUserId, this.admin, ); diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts index 3654dc4fe18..afbf1d86649 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts @@ -34,7 +34,7 @@ describe("AutofillOptionsComponent", () => { let domainSettingsService: MockProxy<DomainSettingsService>; let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>; let platformUtilsService: MockProxy<PlatformUtilsService>; - const getInitialCipherView = jest.fn(() => null); + const getInitialCipherView = jest.fn((): any => null); const formStatusChange$ = new BehaviorSubject<"enabled" | "disabled">("enabled"); beforeEach(async () => { diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts index 32baad189cf..4b0cd0f5f90 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts @@ -20,7 +20,7 @@ describe("CardDetailsSectionComponent", () => { let registerChildFormSpy: jest.SpyInstance; let patchCipherSpy: jest.SpyInstance; - const getInitialCipherView = jest.fn(() => null); + const getInitialCipherView = jest.fn((): any => null); beforeEach(async () => { cipherFormProvider = mock<CipherFormContainer>({ getInitialCipherView }); @@ -67,6 +67,7 @@ describe("CardDetailsSectionComponent", () => { cardView.brand = "Visa"; cardView.expMonth = ""; cardView.code = ""; + cardView.expYear = ""; expect(patchCipherSpy).toHaveBeenCalled(); const patchFn = patchCipherSpy.mock.lastCall[0]; @@ -85,6 +86,7 @@ describe("CardDetailsSectionComponent", () => { cardView.number = ""; cardView.expMonth = ""; cardView.code = ""; + cardView.brand = ""; cardView.expYear = "2022"; expect(patchCipherSpy).toHaveBeenCalled(); @@ -122,8 +124,6 @@ describe("CardDetailsSectionComponent", () => { number, code, brand: cardView.brand, - expMonth: null, - expYear: null, }); }); diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts index a71f57481ff..7b8149b6d7b 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts @@ -52,12 +52,12 @@ export class CardDetailsSectionComponent implements OnInit { * leaving as just null gets inferred as `unknown` */ cardDetailsForm = this.formBuilder.group({ - cardholderName: null as string | null, - number: null as string | null, - brand: null as string | null, - expMonth: null as string | null, - expYear: null as string | number | null, - code: null as string | null, + cardholderName: "", + number: "", + brand: "", + expMonth: "", + expYear: "" as string | number, + code: "", }); /** Available Card Brands */ @@ -110,16 +110,14 @@ export class CardDetailsSectionComponent implements OnInit { .pipe(takeUntilDestroyed()) .subscribe(({ cardholderName, number, brand, expMonth, expYear, code }) => { this.cipherFormContainer.patchCipher((cipher) => { - const expirationYear = normalizeExpiryYearFormat(expYear); + const expirationYear = normalizeExpiryYearFormat(expYear) ?? ""; - Object.assign(cipher.card, { - cardholderName, - number, - brand, - expMonth, - expYear: expirationYear, - code, - }); + cipher.card.cardholderName = cardholderName; + cipher.card.number = number; + cipher.card.brand = brand; + cipher.card.expMonth = expMonth; + cipher.card.expYear = expirationYear; + cipher.card.code = code; return cipher; }); @@ -167,6 +165,7 @@ export class CardDetailsSectionComponent implements OnInit { expMonth: this.initialValues?.expMonth || "", expYear: this.initialValues?.expYear || "", code: this.initialValues?.code || "", + brand: CardView.getCardBrandByPatterns(this.initialValues?.number) || "", }); } @@ -195,18 +194,4 @@ export class CardDetailsSectionComponent implements OnInit { ); } } - - /** Set form initial form values from the current cipher */ - private setInitialValues(cipherView: CipherView) { - const { cardholderName, number, brand, expMonth, expYear, code } = cipherView.card; - - this.cardDetailsForm.setValue({ - cardholderName: cardholderName, - number: number, - brand: brand, - expMonth: expMonth, - expYear: expYear, - code: code, - }); - } } diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts b/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts index 970e6b1fb9a..ac384ee3fd8 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts @@ -37,7 +37,7 @@ describe("CipherFormComponent", () => { provide: CipherFormCacheService, useValue: { init: jest.fn(), getCachedCipherView: jest.fn() }, }, - { provide: ViewCacheService, useValue: { signal: jest.fn(() => () => null) } }, + { provide: ViewCacheService, useValue: { signal: jest.fn(() => (): any => null) } }, { provide: ConfigService, useValue: mock<ConfigService>() }, ], }).compileComponents(); diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts index 12e83b052bd..013ccd6c87e 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts @@ -386,7 +386,7 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { fieldView.type = field.type; fieldView.name = field.name; fieldView.value = value; - fieldView.linkedId = field.linkedId; + fieldView.linkedId = field.linkedId ?? undefined; return fieldView; }); diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.ts b/libs/vault/src/cipher-form/components/identity/identity.component.ts index 119ce1caf6e..4c90024e05a 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.ts +++ b/libs/vault/src/cipher-form/components/identity/identity.component.ts @@ -172,7 +172,7 @@ export class IdentitySectionComponent implements OnInit { populateFormData(cipherView: CipherView) { const { identity } = cipherView; - this.identityForm.setValue({ + this.identityForm.patchValue({ title: identity.title, firstName: identity.firstName, middleName: identity.middleName, diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index ced6c809724..8877b4cbcea 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -312,7 +312,7 @@ export class ItemDetailsSectionComponent implements OnInit { private async initFromExistingCipher(prefillCipher: CipherView) { const { name, folderId, collectionIds } = prefillCipher; - this.itemDetailsForm.setValue({ + this.itemDetailsForm.patchValue({ name: name ? name : (this.initialValues?.name ?? ""), organizationId: prefillCipher.organizationId, // We do not allow changing ownership of an existing cipher. folderId: folderId ? folderId : (this.initialValues?.folderId ?? null), diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts index b07a50fd383..d6fe8a64921 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts @@ -42,7 +42,7 @@ describe("LoginDetailsSectionComponent", () => { let configService: MockProxy<ConfigService>; const collect = jest.fn().mockResolvedValue(null); - const getInitialCipherView = jest.fn(() => null); + const getInitialCipherView = jest.fn((): any => null); beforeEach(async () => { getInitialCipherView.mockClear(); diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index d195ff8b00b..59c583f980b 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -36,7 +36,7 @@ export class DefaultCipherFormService implements CipherFormService { let savedCipher: Cipher; // Creating a new cipher - if (cipher.id == null) { + if (cipher.id == null || cipher.id === "") { const encrypted = await this.cipherService.encrypt(cipher, activeUserId); savedCipher = await this.cipherService.createWithServer(encrypted, config.admin); return await this.cipherService.decrypt(savedCipher, activeUserId); diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index 62425bba7b3..b523c11c7e3 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -12,7 +12,6 @@ </bit-callout> <bit-callout *ngIf="hasLoginUri && hadPendingChangePasswordTask" type="warning" [title]="''"> - <i class="bwi bwi-exclamation-triangle tw-text-warning" aria-hidden="true"></i> <a bitLink href="#" appStopClick (click)="launchChangePassword()"> {{ "changeAtRiskPassword" | i18n }} <i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i> diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts index 2fc35574ba5..7c2afd5029f 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts @@ -44,7 +44,7 @@ import { VaultAutosizeReadOnlyTextArea } from "../../directives/readonly-textare export class CustomFieldV2Component implements OnInit, OnChanges { @Input({ required: true }) cipher!: CipherView; fieldType = FieldType; - fieldOptions: Map<number, LinkedMetadata> | null = null; + fieldOptions: Map<number, LinkedMetadata> | undefined; /** Indexes of hidden fields that are revealed */ revealedHiddenFields: number[] = []; @@ -124,7 +124,7 @@ export class CustomFieldV2Component implements OnInit, OnChanges { case CipherType.Identity: return IdentityView.prototype.linkedFieldOptions; default: - return null; + return undefined; } } } diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index 6ccd0b7ee61..c775d66baac 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -7,7 +7,7 @@ import { toSignal } from "@angular/core/rxjs-interop"; import { fromEvent, map, startWith } from "rxjs"; // eslint-disable-next-line no-restricted-imports -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionTypes, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -102,7 +102,9 @@ export class ItemDetailsV2Component { getIconClass(item: Organization | CollectionView | FolderView): string { if (item instanceof CollectionView) { - return "bwi-collection-shared"; + return item.type === CollectionTypes.DefaultUserCollection + ? "bwi-user" + : "bwi-collection-shared"; } else if (item instanceof FolderView) { return "bwi-folder"; } diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index 453ba93f380..9890074a8c9 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -330,7 +330,8 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI ); }) .map((c) => ({ - icon: "bwi-collection-shared", + icon: + c.type === CollectionTypes.DefaultUserCollection ? "bwi-user" : "bwi-collection-shared", id: c.id, labelName: c.name, listName: c.name, @@ -371,7 +372,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI collection.id !== this.params.activeCollection?.id, ) .map((collection) => ({ - icon: "bwi-collection-shared", + icon: collection.icon, id: collection.id, labelName: collection.labelName, listName: collection.listName, @@ -435,7 +436,8 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI ) .subscribe((collections) => { this.availableCollections = collections.map((c) => ({ - icon: "bwi-collection-shared", + icon: + c.type === CollectionTypes.DefaultUserCollection ? "bwi-user" : "bwi-collection-shared", id: c.id, labelName: c.name, listName: c.name, diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 5acac9ec009..efaefc77ade 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -27,5 +27,3 @@ export { SshImportPromptService } from "./services/ssh-import-prompt.service"; export * from "./abstractions/change-login-password.service"; export * from "./services/default-change-login-password.service"; -export * from "./abstractions/cipher-archive.service"; -export * from "./services/default-cipher-archive.service"; diff --git a/package-lock.json b/package-lock.json index 1b126255e63..77067a48039 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.296", + "@bitwarden/sdk-internal": "0.2.0-main.311", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -35,6 +35,7 @@ "@nx/eslint": "21.3.11", "@nx/jest": "21.3.11", "@nx/js": "21.3.11", + "@nx/webpack": "21.3.11", "big-integer": "1.6.52", "braintree-web-drop-in": "1.44.0", "buffer": "6.0.3", @@ -131,7 +132,7 @@ "copy-webpack-plugin": "13.0.0", "cross-env": "10.0.0", "css-loader": "7.1.2", - "electron": "36.8.1", + "electron": "38.2.0", "electron-builder": "26.0.12", "electron-log": "5.4.0", "electron-reload": "2.0.0-alpha.1", @@ -173,7 +174,7 @@ "ts-loader": "9.5.2", "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", - "typescript": "5.5.4", + "typescript": "5.8.3", "typescript-eslint": "8.31.0", "typescript-strict-plugin": "2.4.4", "url": "0.11.4", @@ -191,11 +192,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.9.0" + "version": "2025.10.0" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2025.9.0", + "version": "2025.10.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "4.0.0", @@ -277,7 +278,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.9.1", + "version": "2025.10.0", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -291,7 +292,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.9.1" + "version": "2025.10.0" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -4688,9 +4689,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.296", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.296.tgz", - "integrity": "sha512-SDTWRwnR+KritfgJVBgWKd27TJxl4IlUdTldVJ/tA0qM5OqGWrY6s4ubtl5eaGIl2X4WYRAvpe+VR93FLakk6A==", + "version": "0.2.0-main.311", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.311.tgz", + "integrity": "sha512-zJdQykNMFOyivpNaCB9jc85wZ1ci2HM8/E4hI+yS7FgRm0sRigK5rieF3+xRjiq7pEsZSD8AucR+u/XK9ADXiw==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" @@ -4808,6 +4809,12 @@ "integrity": "sha512-UIrJB+AfKU0CCfbMoWrsGpd2D/hBpY/SGgFI6WRHPOwhaZ3g9rz1weiJ6eb6L9KgVyunT7s2tckcPkbHw+NzeA==", "license": "MIT" }, + "node_modules/@bufbuild/protobuf": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.9.0.tgz", + "integrity": "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@compodoc/compodoc": { "version": "1.1.26", "resolved": "https://registry.npmjs.org/@compodoc/compodoc/-/compodoc-1.1.26.tgz", @@ -7734,7 +7741,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -7879,7 +7885,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -7916,7 +7921,6 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -7943,7 +7947,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" @@ -7960,7 +7963,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz", "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/base64": "^1.1.1", @@ -7983,7 +7985,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz", "integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" @@ -8028,7 +8029,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, "license": "MIT" }, "node_modules/@listr2/prompt-adapter-inquirer": { @@ -8840,7 +8840,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -8854,7 +8853,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -8864,7 +8862,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -9367,19 +9364,6 @@ } } }, - "node_modules/@nx/eslint/node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/@nx/jest": { "version": "21.3.11", "resolved": "https://registry.npmjs.org/@nx/jest/-/jest-21.3.11.tgz", @@ -10714,6 +10698,517 @@ "win32" ] }, + "node_modules/@nx/webpack": { + "version": "21.3.11", + "resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-21.3.11.tgz", + "integrity": "sha512-GAqA9yHLro4zDf2z27uWseUSLiZZh2IZ3Eh5Kb9l/LA4ujT3whkpNoIo/K2LxzmmOG8k2SkJ7wBntCPk2O1e8g==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.2", + "@nx/devkit": "21.3.11", + "@nx/js": "21.3.11", + "@phenomnomnominal/tsquery": "~5.0.1", + "ajv": "^8.12.0", + "autoprefixer": "^10.4.9", + "babel-loader": "^9.1.2", + "browserslist": "^4.21.4", + "copy-webpack-plugin": "^10.2.4", + "css-loader": "^6.4.0", + "css-minimizer-webpack-plugin": "^5.0.0", + "fork-ts-checker-webpack-plugin": "7.2.13", + "less": "^4.1.3", + "less-loader": "^11.1.0", + "license-webpack-plugin": "^4.0.2", + "loader-utils": "^2.0.3", + "mini-css-extract-plugin": "~2.4.7", + "parse5": "4.0.0", + "picocolors": "^1.1.0", + "postcss": "^8.4.38", + "postcss-import": "~14.1.0", + "postcss-loader": "^6.1.1", + "rxjs": "^7.8.0", + "sass": "^1.85.0", + "sass-embedded": "^1.83.4", + "sass-loader": "^16.0.4", + "source-map-loader": "^5.0.0", + "style-loader": "^3.3.0", + "terser-webpack-plugin": "^5.3.3", + "ts-loader": "^9.3.1", + "tsconfig-paths-webpack-plugin": "4.0.0", + "tslib": "^2.3.0", + "webpack": "~5.99.0", + "webpack-dev-server": "^5.2.1", + "webpack-node-externals": "^3.0.0", + "webpack-subresource-integrity": "^5.1.0" + } + }, + "node_modules/@nx/webpack/node_modules/array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nx/webpack/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@nx/webpack/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@nx/webpack/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@nx/webpack/node_modules/copy-webpack-plugin": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz", + "integrity": "sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^12.0.2", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.20.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/@nx/webpack/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nx/webpack/node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@nx/webpack/node_modules/fork-ts-checker-webpack-plugin": { + "version": "7.2.13", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz", + "integrity": "sha512-fR3WRkOb4bQdWB/y7ssDUlVdrclvwtyCUIHCfivAoYxq9dF7XfrDKbMdZIfwJ7hxIAqkYSGeU7lLJE6xrxIBdg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "vue-template-compiler": "*", + "webpack": "^5.11.0" + }, + "peerDependenciesMeta": { + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/@nx/webpack/node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nx/webpack/node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@nx/webpack/node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@nx/webpack/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@nx/webpack/node_modules/globby": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", + "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", + "license": "MIT", + "dependencies": { + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.9", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nx/webpack/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@nx/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/@nx/webpack/node_modules/less-loader": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.4.tgz", + "integrity": "sha512-6/GrYaB6QcW6Vj+/9ZPgKKs6G10YZai/l/eJ4SLwbzqNTBsAqt5hSLVF47TgsiBxV1P6eAU0GYRH3YRuQU9V3A==", + "license": "MIT", + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/@nx/webpack/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/@nx/webpack/node_modules/mini-css-extract-plugin": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.7.tgz", + "integrity": "sha512-euWmddf0sk9Nv1O0gfeeUAvAkoSlWncNLF77C0TP2+WoPvy8mAHKOzMajcCz2dzvyt3CNgxb1obIEVFIRxaipg==", + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/@nx/webpack/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@nx/webpack/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nx/webpack/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@nx/webpack/node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/@nx/webpack/node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/@nx/webpack/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@nx/webpack/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nx/webpack/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@nx/webpack/node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/@nx/webpack/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@nx/webpack/node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.0.tgz", + "integrity": "sha512-fw/7265mIWukrSHd0i+wSwx64kYUSAKPfxRDksjKIYTxSAp9W9/xcZVBF4Kl0eqQd5eBpAQ/oQrc5RyM/0c1GQ==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tsconfig-paths": "^4.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@nx/webpack/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/@nx/workspace": { "version": "21.3.11", "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.3.11.tgz", @@ -10734,7 +11229,6 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -10774,7 +11268,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10795,7 +11288,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10816,7 +11308,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10837,7 +11328,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10858,7 +11348,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10879,7 +11368,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10900,7 +11388,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10921,7 +11408,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10942,7 +11428,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10963,7 +11448,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10984,7 +11468,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11005,7 +11488,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11026,7 +11508,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11044,7 +11525,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, "license": "Apache-2.0", "optional": true, "bin": { @@ -11058,7 +11538,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -11896,7 +12375,6 @@ "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "devOptional": true, "license": "MIT" }, "node_modules/@sindresorhus/is": { @@ -13183,6 +13661,15 @@ "node": ">= 10" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@ts-morph/common": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz", @@ -13346,7 +13833,6 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -13357,7 +13843,6 @@ "version": "3.5.13", "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -13391,7 +13876,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -13401,7 +13885,6 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "dev": true, "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", @@ -13442,7 +13925,6 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "*", @@ -13453,7 +13935,6 @@ "version": "3.7.7", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, "license": "MIT", "dependencies": { "@types/eslint": "*", @@ -13470,7 +13951,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -13482,7 +13962,6 @@ "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -13546,7 +14025,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/http-assert": { @@ -13567,14 +14046,12 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, "license": "MIT" }, "node_modules/@types/http-proxy": { "version": "1.17.16", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -13822,7 +14299,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -13895,7 +14371,6 @@ "version": "1.3.11", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -13950,14 +14425,12 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { @@ -14009,7 +14482,6 @@ "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -14020,7 +14492,6 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "dev": true, "license": "MIT", "dependencies": { "@types/express": "*" @@ -14030,7 +14501,6 @@ "version": "1.15.8", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -14042,7 +14512,6 @@ "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -14120,7 +14589,6 @@ "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -15049,7 +15517,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", @@ -15060,28 +15527,24 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", @@ -15093,14 +15556,12 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -15113,7 +15574,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" @@ -15123,7 +15583,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" @@ -15133,14 +15592,12 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -15157,7 +15614,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -15171,7 +15627,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -15184,7 +15639,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -15199,7 +15653,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -15274,14 +15727,12 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, "license": "Apache-2.0" }, "node_modules/@yao-pkg/pkg": { @@ -15686,7 +16137,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -15721,7 +16171,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" @@ -15793,7 +16242,6 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, "engines": [ "node >= 0.8.0" ], @@ -16111,7 +16559,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, "license": "MIT" }, "node_modules/array-includes": { @@ -16329,7 +16776,6 @@ "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -16367,7 +16813,6 @@ "version": "4.25.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -16502,7 +16947,6 @@ "version": "9.2.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", - "dev": true, "license": "MIT", "dependencies": { "find-cache-dir": "^4.0.0", @@ -16807,7 +17251,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, "license": "MIT" }, "node_modules/bcryptjs": { @@ -16932,7 +17375,6 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -16942,7 +17384,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17031,7 +17472,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -17042,7 +17482,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, "license": "ISC" }, "node_modules/boolean": { @@ -17218,6 +17657,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-builder": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", + "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", + "license": "MIT/X11" + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -17709,7 +18154,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", @@ -17735,6 +18180,18 @@ "node": ">= 6" } }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001724", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz", @@ -17931,7 +18388,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -17981,7 +18437,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0" @@ -17998,7 +18453,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "devOptional": true, "funding": [ { "type": "github", @@ -18021,7 +18475,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "source-map": "~0.6.0" @@ -18034,7 +18488,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -18320,11 +18774,22 @@ "color-support": "bin.js" } }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, + "license": "MIT" + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", "license": "MIT" }, "node_modules/colors": { @@ -18375,7 +18840,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true, "license": "ISC" }, "node_modules/common-tags": { @@ -18409,7 +18873,6 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" @@ -18422,7 +18885,6 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -18441,7 +18903,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -18451,14 +18912,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/compression/node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -18692,7 +19151,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8" @@ -18784,7 +19242,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", - "dev": true, "license": "MIT", "dependencies": { "is-what": "^3.14.1" @@ -19056,6 +19513,18 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "license": "MIT" }, + "node_modules/css-declaration-sorter": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", + "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, "node_modules/css-loader": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", @@ -19092,11 +19561,54 @@ } } }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", + "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "cssnano": "^6.0.1", + "jest-worker": "^29.4.3", + "postcss": "^8.4.24", + "schema-utils": "^4.0.1", + "serialize-javascript": "^6.0.1" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -19109,11 +19621,23 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -19133,7 +19657,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -19142,6 +19665,115 @@ "node": ">=4" } }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -19590,7 +20222,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, "license": "MIT" }, "node_modules/detect-port": { @@ -19798,7 +20429,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "license": "MIT", "dependencies": { "path-type": "^4.0.0" @@ -19811,7 +20441,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -19927,7 +20556,6 @@ "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "dev": true, "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" @@ -19960,7 +20588,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "utila": "~0.4" @@ -19970,7 +20598,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -19985,7 +20612,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -20012,7 +20638,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -20028,7 +20653,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -20050,7 +20674,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "no-case": "^3.0.4", @@ -20159,9 +20783,9 @@ } }, "node_modules/electron": { - "version": "36.8.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-36.8.1.tgz", - "integrity": "sha512-honaH58/cyCb9QAzIvD+WXWuNIZ0tW9zfBqMz5wZld/rXB+LCTEDb2B3TAv8+pDmlzPlkPio95RkUe86l6MNjg==", + "version": "38.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-38.2.0.tgz", + "integrity": "sha512-Cw5Mb+N5NxsG0Hc1qr8I65Kt5APRrbgTtEEn3zTod30UNJRnAE1xbGk/1NOaDn3ODzI/MYn6BzT9T9zreP7xWA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -20517,7 +21141,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -20555,7 +21178,6 @@ "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -20649,7 +21271,6 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -20759,7 +21380,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { @@ -21514,14 +22134,12 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, "license": "MIT" }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -21835,7 +22453,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -21852,7 +22469,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -21877,7 +22493,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, "funding": [ { "type": "github", @@ -21904,7 +22519,6 @@ "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -21914,7 +22528,6 @@ "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" @@ -22118,7 +22731,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", - "dev": true, "license": "MIT", "dependencies": { "common-path-prefix": "^3.0.0", @@ -22557,7 +23169,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -22707,7 +23318,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", - "dev": true, "license": "Unlicense" }, "node_modules/fs.realpath": { @@ -22941,7 +23551,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/minimatch": { @@ -23154,7 +23763,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, "license": "MIT" }, "node_modules/handlebars": { @@ -23316,7 +23924,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "he": "bin/he" @@ -23372,7 +23980,6 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -23480,7 +24087,7 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", @@ -23513,7 +24120,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 12" @@ -23523,7 +24130,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "camel-case": "^4.1.2", @@ -23668,7 +24275,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true, "license": "MIT" }, "node_modules/http-errors": { @@ -23700,14 +24306,12 @@ "version": "0.5.10", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true, "license": "MIT" }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", @@ -23816,7 +24420,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.18" @@ -23888,7 +24491,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" @@ -23956,7 +24558,6 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", - "dev": true, "license": "MIT", "optional": true, "bin": { @@ -23976,7 +24577,6 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", - "dev": true, "license": "MIT" }, "node_modules/import-fresh": { @@ -24348,7 +24948,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -24612,7 +25211,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", - "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -24872,7 +25470,6 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", - "dev": true, "license": "MIT" }, "node_modules/is-windows": { @@ -26572,7 +27169,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -26590,7 +27186,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -26806,7 +27401,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -26822,7 +27416,6 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -27010,7 +27603,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-typed": { @@ -27195,6 +27787,15 @@ "node": ">=6" } }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/koa": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", @@ -27454,7 +28055,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", - "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.0.0", @@ -27472,7 +28072,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "copy-anything": "^2.0.1", @@ -27526,7 +28125,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -27541,7 +28139,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, "license": "MIT", "optional": true, "bin": { @@ -27555,7 +28152,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -27566,7 +28162,6 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, "license": "ISC", "optional": true, "bin": { @@ -27577,7 +28172,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "optional": true, "engines": { @@ -27610,7 +28204,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", - "dev": true, "license": "ISC", "dependencies": { "webpack-sources": "^3.0.0" @@ -27637,7 +28230,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -28090,7 +28682,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" @@ -28159,7 +28750,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -28168,6 +28758,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -28459,7 +29055,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tslib": "^2.0.3" @@ -28937,6 +29533,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -28950,7 +29552,6 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "dev": true, "license": "Unlicense", "dependencies": { "fs-monkey": "^1.0.4" @@ -28991,7 +29592,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -29001,7 +29601,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -29725,7 +30324,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, "license": "ISC" }, "node_modules/minimatch": { @@ -30169,7 +30767,6 @@ "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dev": true, "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", @@ -30268,7 +30865,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -30315,7 +30911,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -30342,7 +30937,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, "license": "MIT" }, "node_modules/neotraverse": { @@ -30373,7 +30967,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "lower-case": "^2.0.2", @@ -30397,7 +30991,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "dev": true, "license": "MIT" }, "node_modules/node-api-version": { @@ -30935,7 +31528,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -31411,7 +32003,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -32141,7 +32732,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, "license": "MIT" }, "node_modules/oidc-client-ts": { @@ -32173,7 +32763,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -32417,7 +33006,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/retry": "0.12.2", @@ -32435,14 +33023,12 @@ "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "dev": true, "license": "MIT" }, "node_modules/p-retry/node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -32796,7 +33382,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "dot-case": "^3.0.4", @@ -32849,7 +33435,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -32932,7 +33517,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "no-case": "^3.0.4", @@ -33145,7 +33730,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", - "dev": true, "license": "MIT", "dependencies": { "find-up": "^6.3.0" @@ -33161,7 +33745,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^7.1.0", @@ -33178,7 +33761,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^6.0.0" @@ -33194,7 +33776,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^1.0.0" @@ -33210,7 +33791,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^4.0.0" @@ -33226,7 +33806,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -33236,7 +33815,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.20" @@ -33413,7 +33991,6 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -33438,6 +34015,117 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-calc/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, "node_modules/postcss-import": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", @@ -33551,11 +34239,134 @@ "dev": true, "license": "MIT" }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-modules-extract-imports": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" @@ -33568,7 +34379,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", @@ -33586,7 +34396,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, "license": "ISC", "dependencies": { "postcss-selector-parser": "^7.0.0" @@ -33602,7 +34411,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, "license": "ISC", "dependencies": { "icss-utils": "^5.0.0" @@ -33654,11 +34462,234 @@ "node": ">=4" } }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, "node_modules/postcss-selector-parser": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -33672,7 +34703,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/postject": { @@ -33840,7 +34870,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "lodash": "^4.17.20", @@ -34035,7 +35065,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, "license": "MIT", "optional": true }, @@ -34128,7 +35157,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -34162,7 +35190,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -34276,7 +35303,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -34286,7 +35312,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -34323,7 +35348,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -34520,7 +35544,7 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -34595,7 +35619,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "css-select": "^4.1.3", @@ -34609,7 +35633,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -34626,7 +35650,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", @@ -34641,7 +35665,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" @@ -34657,7 +35681,7 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", @@ -34672,7 +35696,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -34682,7 +35706,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, + "devOptional": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -34711,7 +35735,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -34942,7 +35965,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -35094,7 +36116,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -35282,7 +36303,6 @@ "version": "1.88.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", - "dev": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -35299,11 +36319,397 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/sass-embedded": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.93.2.tgz", + "integrity": "sha512-FvQdkn2dZ8DGiLgi0Uf4zsj7r/BsiLImNa5QJ10eZalY6NfZyjrmWGFcuCN5jNwlDlXFJnftauv+UtvBKLvepQ==", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.5.0", + "buffer-builder": "^0.2.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.0.2", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-all-unknown": "1.93.2", + "sass-embedded-android-arm": "1.93.2", + "sass-embedded-android-arm64": "1.93.2", + "sass-embedded-android-riscv64": "1.93.2", + "sass-embedded-android-x64": "1.93.2", + "sass-embedded-darwin-arm64": "1.93.2", + "sass-embedded-darwin-x64": "1.93.2", + "sass-embedded-linux-arm": "1.93.2", + "sass-embedded-linux-arm64": "1.93.2", + "sass-embedded-linux-musl-arm": "1.93.2", + "sass-embedded-linux-musl-arm64": "1.93.2", + "sass-embedded-linux-musl-riscv64": "1.93.2", + "sass-embedded-linux-musl-x64": "1.93.2", + "sass-embedded-linux-riscv64": "1.93.2", + "sass-embedded-linux-x64": "1.93.2", + "sass-embedded-unknown-all": "1.93.2", + "sass-embedded-win32-arm64": "1.93.2", + "sass-embedded-win32-x64": "1.93.2" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.93.2.tgz", + "integrity": "sha512-GdEuPXIzmhRS5J7UKAwEvtk8YyHQuFZRcpnEnkA3rwRUI27kwjyXkNeIj38XjUQ3DzrfMe8HcKFaqWGHvblS7Q==", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "sass": "1.93.2" + } + }, + "node_modules/sass-embedded-all-unknown/node_modules/sass": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", + "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.93.2.tgz", + "integrity": "sha512-I8bpO8meZNo5FvFx5FIiE7DGPVOYft0WjuwcCCdeJ6duwfkl6tZdatex1GrSigvTsuz9L0m4ngDcX/Tj/8yMow==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.93.2.tgz", + "integrity": "sha512-346f4iVGAPGcNP6V6IOOFkN5qnArAoXNTPr5eA/rmNpeGwomdb7kJyQ717r9rbJXxOG8OAAUado6J0qLsjnjXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.93.2.tgz", + "integrity": "sha512-hSMW1s4yJf5guT9mrdkumluqrwh7BjbZ4MbBW9tmi1DRDdlw1Wh9Oy1HnnmOG8x9XcI1qkojtPL6LUuEJmsiDg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.93.2.tgz", + "integrity": "sha512-JqktiHZduvn+ldGBosE40ALgQ//tGCVNAObgcQ6UIZznEJbsHegqStqhRo8UW3x2cgOO2XYJcrInH6cc7wdKbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.93.2.tgz", + "integrity": "sha512-qI1X16qKNeBJp+M/5BNW7v/JHCDYWr1/mdoJ7+UMHmP0b5AVudIZtimtK0hnjrLnBECURifd6IkulybR+h+4UA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.93.2.tgz", + "integrity": "sha512-4KeAvlkQ0m0enKUnDGQJZwpovYw99iiMb8CTZRSsQm8Eh7halbJZVmx67f4heFY/zISgVOCcxNg19GrM5NTwtA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.93.2.tgz", + "integrity": "sha512-N3+D/ToHtzwLDO+lSH05Wo6/KRxFBPnbjVHASOlHzqJnK+g5cqex7IFAp6ozzlRStySk61Rp6d+YGrqZ6/P0PA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.93.2.tgz", + "integrity": "sha512-9ftX6nd5CsShJqJ2WRg+ptaYvUW+spqZfJ88FbcKQBNFQm6L87luj3UI1rB6cP5EWrLwHA754OKxRJyzWiaN6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.93.2.tgz", + "integrity": "sha512-XBTvx66yRenvEsp3VaJCb3HQSyqCsUh7R+pbxcN5TuzueybZi0LXvn9zneksdXcmjACMlMpIVXi6LyHPQkYc8A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.93.2.tgz", + "integrity": "sha512-+3EHuDPkMiAX5kytsjEC1bKZCawB9J6pm2eBIzzLMPWbf5xdx++vO1DpT7hD4bm4ZGn0eVHgSOKIfP6CVz6tVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.93.2.tgz", + "integrity": "sha512-0sB5kmVZDKTYzmCSlTUnjh6mzOhzmQiW/NNI5g8JS4JiHw2sDNTvt1dsFTuqFkUHyEOY3ESTsfHHBQV8Ip4bEA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.93.2.tgz", + "integrity": "sha512-t3ejQ+1LEVuHy7JHBI2tWHhoMfhedUNDjGJR2FKaLgrtJntGnyD1RyX0xb3nuqL/UXiEAtmTmZY+Uh3SLUe1Hg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.93.2.tgz", + "integrity": "sha512-e7AndEwAbFtXaLy6on4BfNGTr3wtGZQmypUgYpSNVcYDO+CWxatKVY4cxbehMPhxG9g5ru+eaMfynvhZt7fLaA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.93.2.tgz", + "integrity": "sha512-U3EIUZQL11DU0xDDHXexd4PYPHQaSQa2hzc4EzmhHqrAj+TyfYO94htjWOd+DdTPtSwmLp+9cTWwPZBODzC96w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-unknown-all": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.93.2.tgz", + "integrity": "sha512-7VnaOmyewcXohiuoFagJ3SK5ddP9yXpU0rzz+pZQmS1/+5O6vzyFCUoEt3HDRaLctH4GT3nUGoK1jg0ae62IfQ==", + "license": "MIT", + "optional": true, + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "dependencies": { + "sass": "1.93.2" + } + }, + "node_modules/sass-embedded-unknown-all/node_modules/sass": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", + "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.93.2.tgz", + "integrity": "sha512-Y90DZDbQvtv4Bt0GTXKlcT9pn4pz8AObEjFF8eyul+/boXwyptPZ/A1EyziAeNaIEIfxyy87z78PUgCeGHsx3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.93.2.tgz", + "integrity": "sha512-BbSucRP6PVRZGIwlEBkp+6VQl2GWdkWFMN+9EuOTPrLxCJZoq+yhzmbjspd3PeM8+7WJ7AdFu/uRYdO8tor1iQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/sass-loader": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", - "dev": true, "license": "MIT", "dependencies": { "neo-async": "^2.6.2" @@ -35344,7 +36750,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/saxes": { @@ -35373,7 +36779,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -35393,7 +36798,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -35411,14 +36815,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true, "license": "MIT" }, "node_modules/selfsigned": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/node-forge": "^1.3.0", @@ -35505,7 +36907,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" @@ -35515,7 +36916,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.4", @@ -35534,7 +36934,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -35548,7 +36947,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -35558,7 +36956,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -35568,7 +36965,6 @@ "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dev": true, "license": "MIT", "dependencies": { "depd": "~1.1.2", @@ -35584,14 +36980,12 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, "license": "ISC" }, "node_modules/serve-index/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -35601,7 +36995,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -35614,14 +37007,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/serve-index/node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -35631,14 +37022,12 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true, "license": "ISC" }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -35771,7 +37160,6 @@ "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -35996,7 +37384,6 @@ "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, "license": "MIT", "dependencies": { "faye-websocket": "^0.11.3", @@ -36008,7 +37395,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -36061,7 +37447,6 @@ "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">= 8" @@ -36071,7 +37456,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -36081,7 +37465,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", - "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "^0.6.3", @@ -36102,7 +37485,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -36113,7 +37495,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -36317,7 +37698,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.0", @@ -36334,7 +37714,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.0", @@ -36349,7 +37728,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -36761,6 +38139,35 @@ "webpack": "^5.27.0" } }, + "node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/stylehacks/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -36928,6 +38335,40 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -36944,6 +38385,27 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", + "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", @@ -37096,7 +38558,6 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -37320,7 +38781,6 @@ "version": "5.39.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -37339,7 +38799,6 @@ "version": "5.3.14", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", @@ -37374,7 +38833,6 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -37389,7 +38847,6 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -37405,7 +38862,6 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, "license": "MIT" }, "node_modules/test-exclude": { @@ -37492,7 +38948,6 @@ "version": "1.21.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", - "dev": true, "license": "Unlicense", "engines": { "node": ">=10.18" @@ -37511,7 +38966,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true, "license": "MIT" }, "node_modules/tiny-async-pool": { @@ -37688,7 +39142,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", "integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" @@ -37843,7 +39296,6 @@ "version": "9.5.2", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -38522,7 +39974,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", - "dev": true, "license": "MIT" }, "node_modules/typedarray": { @@ -38542,9 +39993,9 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -39107,14 +40558,13 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -39181,6 +40631,12 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -39826,7 +41282,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dev": true, "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -39840,7 +41295,6 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" @@ -39876,7 +41330,6 @@ "version": "5.99.7", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz", "integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==", - "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", @@ -39977,7 +41430,6 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", - "dev": true, "license": "MIT", "dependencies": { "colorette": "^2.0.10", @@ -40007,7 +41459,6 @@ "version": "4.17.2", "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/json-pack": "^1.0.3", @@ -40027,7 +41478,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -40037,7 +41487,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -40050,7 +41499,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.1.tgz", "integrity": "sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", @@ -40108,7 +41556,6 @@ "version": "4.17.23", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -40121,7 +41568,6 @@ "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -40134,7 +41580,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -40148,7 +41593,6 @@ "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -40173,7 +41617,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -40198,7 +41641,6 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -40211,7 +41653,6 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -40221,14 +41662,12 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, "license": "MIT" }, "node_modules/webpack-dev-server/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -40238,14 +41677,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/webpack-dev-server/node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -40292,7 +41729,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -40311,7 +41747,6 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -40321,7 +41756,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -40334,7 +41768,6 @@ "version": "2.0.9", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", @@ -40359,7 +41792,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -40372,7 +41804,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -40382,7 +41813,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -40395,7 +41825,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -40405,7 +41834,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -40415,7 +41843,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -40428,7 +41855,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -40438,7 +41864,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -40451,7 +41876,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -40461,14 +41885,12 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, "license": "MIT" }, "node_modules/webpack-dev-server/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -40481,7 +41903,6 @@ "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" @@ -40497,7 +41918,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -40513,7 +41933,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -40526,7 +41945,6 @@ "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -40551,7 +41969,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -40561,7 +41978,6 @@ "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -40577,7 +41993,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -40587,7 +42002,6 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -40628,7 +42042,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -40638,7 +42051,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" @@ -40648,7 +42060,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", - "dev": true, "license": "MIT", "dependencies": { "typed-assert": "^1.0.8" @@ -40677,7 +42088,6 @@ "version": "4.25.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -40710,7 +42120,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -40724,7 +42133,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -40734,14 +42142,12 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -40751,7 +42157,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -40764,7 +42169,6 @@ "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", @@ -40779,7 +42183,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=0.8.0" diff --git a/package.json b/package.json index e94d0e98522..aec6ddb9536 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "copy-webpack-plugin": "13.0.0", "cross-env": "10.0.0", "css-loader": "7.1.2", - "electron": "36.8.1", + "electron": "38.2.0", "electron-builder": "26.0.12", "electron-log": "5.4.0", "electron-reload": "2.0.0-alpha.1", @@ -137,7 +137,7 @@ "ts-loader": "9.5.2", "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", - "typescript": "5.5.4", + "typescript": "5.8.3", "typescript-eslint": "8.31.0", "typescript-strict-plugin": "2.4.4", "url": "0.11.4", @@ -158,7 +158,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.296", + "@bitwarden/sdk-internal": "0.2.0-main.311", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -170,6 +170,7 @@ "@nx/eslint": "21.3.11", "@nx/jest": "21.3.11", "@nx/js": "21.3.11", + "@nx/webpack": "21.3.11", "big-integer": "1.6.52", "braintree-web-drop-in": "1.44.0", "buffer": "6.0.3",