mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 21:20:27 +00:00
Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows
This commit is contained in:
236
.github/workflows/build-web.yml
vendored
236
.github/workflows/build-web.yml
vendored
@@ -42,10 +42,11 @@ on:
|
||||
env:
|
||||
_AZ_REGISTRY: bitwardenprod.azurecr.io
|
||||
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.value }}
|
||||
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
@@ -54,7 +55,7 @@ jobs:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Get GitHub sha as version
|
||||
id: version
|
||||
@@ -75,133 +76,61 @@ jobs:
|
||||
has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
|
||||
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
|
||||
|
||||
build-artifacts:
|
||||
name: Build artifacts
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- setup
|
||||
env:
|
||||
_VERSION: ${{ needs.setup.outputs.version }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: "selfhosted-open-source"
|
||||
npm_command: "dist:oss:selfhost"
|
||||
- name: "cloud-COMMERCIAL"
|
||||
npm_command: "dist:bit:cloud"
|
||||
- name: "selfhosted-COMMERCIAL"
|
||||
npm_command: "dist:bit:selfhost"
|
||||
- name: "cloud-QA"
|
||||
npm_command: "build:bit:qa"
|
||||
git_metadata: true
|
||||
- name: "ee"
|
||||
npm_command: "build:bit:ee"
|
||||
git_metadata: true
|
||||
- name: "cloud-euprd"
|
||||
npm_command: "build:bit:euprd"
|
||||
- name: "cloud-euqa"
|
||||
npm_command: "build:bit:euqa"
|
||||
git_metadata: true
|
||||
- name: "cloud-usdev"
|
||||
npm_command: "build:bit:usdev"
|
||||
git_metadata: true
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: ${{ env._NODE_VERSION }}
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
whoami
|
||||
node --version
|
||||
npm --version
|
||||
docker --version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download SDK Artifacts
|
||||
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
artifacts: sdk-internal
|
||||
repo: bitwarden/sdk-internal
|
||||
path: ../sdk-internal
|
||||
if_no_artifact_found: fail
|
||||
|
||||
- name: Override SDK
|
||||
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
|
||||
working-directory: ./
|
||||
run: |
|
||||
ls -l ../
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Add Git metadata to build version
|
||||
working-directory: apps/web
|
||||
if: matrix.git_metadata
|
||||
run: |
|
||||
VERSION=$( jq -r ".version" package.json)
|
||||
jq --arg version "$VERSION+${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
|
||||
- name: Build ${{ matrix.name }}
|
||||
working-directory: apps/web
|
||||
run: npm run ${{ matrix.npm_command }}
|
||||
|
||||
- name: Package artifact
|
||||
working-directory: apps/web
|
||||
run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build
|
||||
|
||||
- name: Upload ${{ matrix.name }} artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: web-${{ env._VERSION }}-${{ matrix.name }}.zip
|
||||
path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
build-containers:
|
||||
name: Build Docker images
|
||||
runs-on: ubuntu-22.04
|
||||
name: Build artifacts and container images
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
security-events: write
|
||||
id-token: write
|
||||
needs:
|
||||
- setup
|
||||
- build-artifacts
|
||||
needs: setup
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- artifact_name: cloud-QA
|
||||
image_name: web-qa-cloud
|
||||
- artifact_name: ee
|
||||
image_name: web-ee
|
||||
- artifact_name: selfhosted-open-source
|
||||
image_name: web-oss
|
||||
npm_command: dist:oss:selfhost
|
||||
- artifact_name: cloud-COMMERCIAL
|
||||
image_name: web-cloud
|
||||
npm_command: dist:bit:cloud
|
||||
- artifact_name: selfhosted-COMMERCIAL
|
||||
image_name: web
|
||||
npm_command: dist:bit:selfhost
|
||||
- artifact_name: cloud-QA
|
||||
image_name: web-qa-cloud
|
||||
npm_command: build:bit:qa
|
||||
git_metadata: true
|
||||
- artifact_name: ee
|
||||
image_name: web-ee
|
||||
npm_command: build:bit:ee
|
||||
git_metadata: true
|
||||
- artifact_name: cloud-euprd
|
||||
image_name: web-euprd
|
||||
npm_command: build:bit:euprd
|
||||
- artifact_name: cloud-euqa
|
||||
image_name: web-euqa
|
||||
npm_command: build:bit:euqa
|
||||
git_metadata: true
|
||||
- artifact_name: cloud-usdev
|
||||
image_name: web-usdev
|
||||
npm_command: build:bit:usdev
|
||||
git_metadata: true
|
||||
env:
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
_VERSION: ${{ needs.setup.outputs.version }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Check out Server repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
path: server
|
||||
repository: bitwarden/server
|
||||
ref: ${{ github.event.pull_request.head.sha && 'main' || github.ref }}
|
||||
|
||||
- name: Check Branch to Publish
|
||||
env:
|
||||
@@ -216,6 +145,21 @@ jobs:
|
||||
echo "is_publish_branch=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Add Git metadata to build version
|
||||
working-directory: apps/web
|
||||
if: matrix.git_metadata
|
||||
run: |
|
||||
VERSION=$( jq -r ".version" package.json)
|
||||
jq --arg version "$VERSION+${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
|
||||
########## Set up Docker ##########
|
||||
- name: Set up QEMU emulators
|
||||
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||
|
||||
########## ACRs ##########
|
||||
- name: Login to Prod Azure
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
@@ -225,7 +169,7 @@ jobs:
|
||||
|
||||
- name: Log into Prod container registry
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
run: az acr login -n bitwardenprod
|
||||
run: az acr login -n ${_AZ_REGISTRY%.azurecr.io}
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
@@ -241,14 +185,8 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Download ${{ matrix.artifact_name }} artifact
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
with:
|
||||
name: web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip
|
||||
path: apps/web
|
||||
|
||||
########## Generate image tag and build Docker image ##########
|
||||
- name: Generate Docker image tag
|
||||
- name: Generate container image tag
|
||||
id: tag
|
||||
run: |
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
|
||||
@@ -270,10 +208,6 @@ jobs:
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
########## Build Image ##########
|
||||
- name: Extract artifact
|
||||
working-directory: apps/web
|
||||
run: unzip web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip
|
||||
|
||||
- name: Generate image full name
|
||||
id: image-name
|
||||
env:
|
||||
@@ -283,16 +217,38 @@ jobs:
|
||||
|
||||
- name: Build Docker image
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
id: build-docker
|
||||
id: build-container
|
||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
||||
with:
|
||||
context: apps/web
|
||||
build-args: |
|
||||
NODE_VERSION=${{ env._NODE_VERSION }}
|
||||
NPM_COMMAND=${{ matrix.npm_command }}
|
||||
context: .
|
||||
file: apps/web/Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: |
|
||||
linux/amd64,
|
||||
linux/arm/v7,
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.image-name.outputs.name }}
|
||||
secrets: |
|
||||
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
||||
|
||||
- name: Zip project
|
||||
working-directory: apps/web
|
||||
env:
|
||||
IMAGE_NAME: ${{ steps.image-name.outputs.name }}
|
||||
run: |
|
||||
mkdir build
|
||||
docker run --rm --volume $(pwd)/build:/temp --entrypoint bash \
|
||||
$IMAGE_NAME -c "cp -r ./ /temp"
|
||||
|
||||
zip -r web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip build
|
||||
|
||||
- name: Upload ${{ matrix.artifact_name }} artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip
|
||||
path: apps/web/web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install Cosign
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
@@ -301,7 +257,7 @@ jobs:
|
||||
- name: Sign image with Cosign
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
env:
|
||||
DIGEST: ${{ steps.build-docker.outputs.digest }}
|
||||
DIGEST: ${{ steps.build-container.outputs.digest }}
|
||||
TAGS: ${{ steps.image-name.outputs.name }}
|
||||
run: |
|
||||
IFS="," read -a tags <<< "${TAGS}"
|
||||
@@ -329,19 +285,19 @@ jobs:
|
||||
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||
|
||||
- name: Log out of Docker
|
||||
run: docker logout
|
||||
run: docker logout $_AZ_REGISTRY
|
||||
|
||||
|
||||
crowdin-push:
|
||||
name: Crowdin Push
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- build-artifacts
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-containers
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@@ -367,12 +323,12 @@ jobs:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
|
||||
trigger-web-vault-deploy:
|
||||
name: Trigger web vault deploy
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build-artifacts
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-containers
|
||||
steps:
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@@ -402,13 +358,13 @@ jobs:
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
check-failures:
|
||||
name: Check for failures
|
||||
if: always()
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- setup
|
||||
- build-artifacts
|
||||
- build-containers
|
||||
- crowdin-push
|
||||
- trigger-web-vault-deploy
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
|
||||
@Component({
|
||||
selector: "app-sso",
|
||||
templateUrl: "sso-v1.component.html",
|
||||
})
|
||||
export class SsoComponentV1 extends BaseSsoComponent {
|
||||
constructor(
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
syncService: SyncService,
|
||||
environmentService: EnvironmentService,
|
||||
logService: LogService,
|
||||
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
configService: ConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
@Inject(WINDOW) private win: Window,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
ssoLoginService,
|
||||
loginStrategyService,
|
||||
router,
|
||||
i18nService,
|
||||
route,
|
||||
stateService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
logService,
|
||||
userDecryptionOptionsService,
|
||||
configService,
|
||||
masterPasswordService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
|
||||
environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
|
||||
this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html";
|
||||
});
|
||||
this.clientId = "browser";
|
||||
|
||||
this.onSuccessfulLogin = async () => {
|
||||
// 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
|
||||
syncService.fullSync(true);
|
||||
|
||||
// If the vault is unlocked then this will clear keys from memory, which we don't want to do
|
||||
if ((await this.authService.getAuthStatus()) !== AuthenticationStatus.Unlocked) {
|
||||
BrowserApi.reloadOpenWindows();
|
||||
}
|
||||
|
||||
this.win.close();
|
||||
};
|
||||
|
||||
this.onSuccessfulLoginTde = async () => {
|
||||
// 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
|
||||
syncService.fullSync(true);
|
||||
};
|
||||
|
||||
this.onSuccessfulLoginTdeNavigate = async () => {
|
||||
this.win.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data";
|
||||
@@ -25,8 +23,6 @@ import { OverlayNotificationsBackground } from "./overlay-notifications.backgrou
|
||||
|
||||
describe("OverlayNotificationsBackground", () => {
|
||||
let logService: MockProxy<LogService>;
|
||||
let getFeatureFlagMock$: BehaviorSubject<boolean>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let notificationBackground: NotificationBackground;
|
||||
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
|
||||
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
|
||||
@@ -35,10 +31,6 @@ describe("OverlayNotificationsBackground", () => {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
logService = mock<LogService>();
|
||||
getFeatureFlagMock$ = new BehaviorSubject(true);
|
||||
configService = mock<ConfigService>({
|
||||
getFeatureFlag$: jest.fn().mockReturnValue(getFeatureFlagMock$),
|
||||
});
|
||||
notificationBackground = mock<NotificationBackground>();
|
||||
getEnableChangedPasswordPromptSpy = jest
|
||||
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
|
||||
@@ -48,10 +40,8 @@ describe("OverlayNotificationsBackground", () => {
|
||||
.mockResolvedValue(true);
|
||||
overlayNotificationsBackground = new OverlayNotificationsBackground(
|
||||
logService,
|
||||
configService,
|
||||
notificationBackground,
|
||||
);
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
await overlayNotificationsBackground.init();
|
||||
});
|
||||
|
||||
@@ -60,27 +50,6 @@ describe("OverlayNotificationsBackground", () => {
|
||||
jest.clearAllTimers();
|
||||
});
|
||||
|
||||
describe("feature flag behavior", () => {
|
||||
let runtimeRemoveListenerSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
runtimeRemoveListenerSpy = jest.spyOn(chrome.runtime.onMessage, "removeListener");
|
||||
});
|
||||
|
||||
it("removes the extension listeners if the current flag value is set to `false`", () => {
|
||||
getFeatureFlagMock$.next(false);
|
||||
|
||||
expect(runtimeRemoveListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores the feature flag change if the previous flag value is equal to the current flag value", () => {
|
||||
getFeatureFlagMock$.next(false);
|
||||
getFeatureFlagMock$.next(false);
|
||||
|
||||
expect(runtimeRemoveListenerSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setting up the form submission listeners", () => {
|
||||
let fields: MockProxy<AutofillField>[];
|
||||
let details: MockProxy<AutofillPageDetails>;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { startWith, Subject, Subscription, switchMap, timer } from "rxjs";
|
||||
import { pairwise } from "rxjs/operators";
|
||||
import { Subject, switchMap, timer } from "rxjs";
|
||||
|
||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
@@ -26,7 +23,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map();
|
||||
private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set();
|
||||
private modifyLoginCipherFormData: ModifyLoginCipherFormDataForTab = new Map();
|
||||
private featureFlagState$: Subscription;
|
||||
private clearLoginCipherFormDataSubject: Subject<void> = new Subject();
|
||||
private notificationFallbackTimeout: number | NodeJS.Timeout | null;
|
||||
private readonly formSubmissionRequestMethods: Set<string> = new Set(["POST", "PUT", "PATCH"]);
|
||||
@@ -38,7 +34,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private configService: ConfigService,
|
||||
private notificationBackground: NotificationBackground,
|
||||
) {}
|
||||
|
||||
@@ -46,35 +41,13 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
* Initialize the overlay notifications background service.
|
||||
*/
|
||||
async init() {
|
||||
this.featureFlagState$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.NotificationBarAddLoginImprovements)
|
||||
.pipe(startWith(undefined), pairwise())
|
||||
.subscribe(([prev, current]) => this.handleInitFeatureFlagChange(prev, current));
|
||||
this.setupExtensionListeners();
|
||||
|
||||
this.clearLoginCipherFormDataSubject
|
||||
.pipe(switchMap(() => timer(CLEAR_NOTIFICATION_LOGIN_DATA_DURATION)))
|
||||
.subscribe(() => this.modifyLoginCipherFormData.clear());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles enabling/disabling the extension listeners that trigger the
|
||||
* overlay notifications based on the feature flag state.
|
||||
*
|
||||
* @param previousValue - The previous value of the feature flag
|
||||
* @param currentValue - The current value of the feature flag
|
||||
*/
|
||||
private handleInitFeatureFlagChange = (previousValue: boolean, currentValue: boolean) => {
|
||||
if (previousValue === currentValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentValue) {
|
||||
this.setupExtensionListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeExtensionListeners();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the response from the content script with the page details. Triggers an initialization
|
||||
* of the add login or change password notification if the conditions are met.
|
||||
@@ -520,15 +493,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
chrome.tabs.onUpdated.addListener(this.handleTabUpdated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listeners for the extension messages and the tab events.
|
||||
*/
|
||||
private removeExtensionListeners() {
|
||||
BrowserApi.removeListener(chrome.runtime.onMessage, this.handleExtensionMessage);
|
||||
chrome.tabs.onRemoved.removeListener(this.handleTabRemoved);
|
||||
chrome.tabs.onUpdated.removeListener(this.handleTabUpdated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles messages that are sent to the extension background.
|
||||
*
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@ const NotificationTypes = {
|
||||
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
|
||||
|
||||
type NotificationBarIframeInitData = {
|
||||
applyRedesign?: boolean;
|
||||
ciphers?: NotificationCipherData[];
|
||||
folders?: FolderView[];
|
||||
importType?: string;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<body>
|
||||
<div id="notification-bar-outer-wrapper" class="outer-wrapper">
|
||||
<div class="logo">
|
||||
<div class="logo-wrapper">
|
||||
<a href="https://vault.bitwarden.com" target="_blank" id="logo-link" rel="noreferrer">
|
||||
<img id="logo" alt="Bitwarden" />
|
||||
</a>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@import "../shared/styles/variables";
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
@@ -14,16 +14,30 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
font-size: $font-size-base;
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
|
||||
.outer-wrapper {
|
||||
padding: 0 10px;
|
||||
border-bottom: 2px solid transparent;
|
||||
display: grid;
|
||||
grid-template-columns: 24px auto 55px;
|
||||
grid-column-gap: 10px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
min-height: 42px;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@include themify($themes) {
|
||||
border-color: themed("borderColor");
|
||||
border-bottom-color: themed("primaryColor");
|
||||
}
|
||||
|
||||
@@ -50,20 +64,34 @@ body {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#logo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#close-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
border-color: rgba(themed("textColor"), 0.2);
|
||||
background-color: rgba(themed("textColor"), 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#close {
|
||||
@@ -78,15 +106,79 @@ img {
|
||||
}
|
||||
}
|
||||
|
||||
#close-button:hover {
|
||||
@include themify($themes) {
|
||||
border-color: rgba(themed("textColor"), 0.2);
|
||||
background-color: rgba(themed("textColor"), 0.2);
|
||||
.notification-close {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
#content .inner-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
.notification-body {
|
||||
width: 100%;
|
||||
padding: 4px 38px 24px 42px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.notification-actions {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
|
||||
#never-save {
|
||||
margin-right: auto;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
#select-folder {
|
||||
width: 125px;
|
||||
margin-right: 6px;
|
||||
font-size: 12px;
|
||||
appearance: none;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right 4px;
|
||||
background-size: 16px;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedTextColor");
|
||||
border-color: themed("mutedTextColor");
|
||||
}
|
||||
|
||||
&:not([disabled]) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.primary,
|
||||
.secondary {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
margin-right: 6px;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&.success-message,
|
||||
&.error-message {
|
||||
padding: 4px 36px 6px 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.35rem 15px;
|
||||
padding: 4px 8px;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
@@ -133,14 +225,13 @@ button.neutral {
|
||||
text-decoration: underline;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: transparent;
|
||||
color: darken(themed("primaryColor"), 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0.35rem;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid #000000;
|
||||
border-radius: $border-radius;
|
||||
|
||||
@@ -151,16 +242,9 @@ select {
|
||||
}
|
||||
}
|
||||
|
||||
select,
|
||||
button {
|
||||
font-size: $font-size-base;
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
|
||||
@include themify($themes) {
|
||||
@@ -184,6 +268,13 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.success-event,
|
||||
.error-event {
|
||||
.notification-body {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
#select-folder {
|
||||
display: none;
|
||||
@@ -196,131 +287,8 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.notification-bar-redesign {
|
||||
button {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.outer-wrapper {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
border-top: 1px solid transparent;
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
|
||||
@include themify($themes) {
|
||||
border-top-color: themed("borderColor");
|
||||
border-left-color: themed("borderColor");
|
||||
border-right-color: themed("borderColor");
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notification-close {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
|
||||
#close-button {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#content .inner-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.notification-body {
|
||||
width: 100%;
|
||||
padding: 4px 38px 24px 42px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.notification-actions {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
|
||||
#never-save {
|
||||
margin-right: auto;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
#select-folder {
|
||||
width: 125px;
|
||||
margin-right: 6px;
|
||||
font-size: 12px;
|
||||
appearance: none;
|
||||
background-size: 16px;
|
||||
background-position: center right 4px;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedTextColor");
|
||||
border-color: themed("mutedTextColor");
|
||||
}
|
||||
|
||||
&:not([disabled]) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.primary,
|
||||
.secondary {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
margin-right: 6px;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&.success-message,
|
||||
&.error-message {
|
||||
padding: 4px 36px 6px 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.success-event,
|
||||
.error-event {
|
||||
.notification-body {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme_light {
|
||||
.notification-bar-redesign #content .inner-wrapper {
|
||||
#content .inner-wrapper {
|
||||
#select-folder {
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB3aWR0aD0nMTYnIGhlaWdodD0nMTYnIGZpbGw9J25vbmUnPjxwYXRoIHN0cm9rZT0nIzIxMjUyOScgZD0nbTUgNiAzIDMgMy0zJy8+PC9zdmc+");
|
||||
}
|
||||
@@ -328,7 +296,7 @@ button {
|
||||
}
|
||||
|
||||
.theme_dark {
|
||||
.notification-bar-redesign #content .inner-wrapper {
|
||||
#content .inner-wrapper {
|
||||
#select-folder {
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNicgaGVpZ2h0PScxNicgZmlsbD0nbm9uZSc+PHBhdGggc3Ryb2tlPScjZmZmZmZmJyBkPSdtNSA2IDMgMyAzLTMnLz48L3N2Zz4=");
|
||||
}
|
||||
|
||||
@@ -507,10 +507,6 @@ function setNotificationBarTheme() {
|
||||
const theme = getTheme(globalThis, notificationBarIframeInitData.theme);
|
||||
|
||||
document.documentElement.classList.add(`theme_${theme}`);
|
||||
|
||||
if (notificationBarIframeInitData.applyRedesign) {
|
||||
document.body.classList.add("notification-bar-redesign");
|
||||
}
|
||||
}
|
||||
|
||||
function postMessageToParent(message: NotificationBarWindowMessage) {
|
||||
|
||||
@@ -17,6 +17,7 @@ export class OverlayNotificationsContentService
|
||||
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
||||
private currentNotificationBarType: string | null = null;
|
||||
private removeTabFromNotificationQueueTypes = new Set(["add", "change"]);
|
||||
private notificationRefreshFlag: boolean;
|
||||
private notificationBarElementStyles: Partial<CSSStyleDeclaration> = {
|
||||
height: "82px",
|
||||
width: "430px",
|
||||
@@ -54,6 +55,9 @@ export class OverlayNotificationsContentService
|
||||
|
||||
constructor() {
|
||||
void sendExtensionMessage("checkNotificationQueue");
|
||||
void sendExtensionMessage("notificationRefreshFlagValue").then((notificationRefreshFlag) => {
|
||||
this.notificationRefreshFlag = !!notificationRefreshFlag;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +88,6 @@ export class OverlayNotificationsContentService
|
||||
theme: typeData.theme,
|
||||
removeIndividualVault: typeData.removeIndividualVault,
|
||||
importType: typeData.importType,
|
||||
applyRedesign: true,
|
||||
launchTimestamp: typeData.launchTimestamp,
|
||||
};
|
||||
|
||||
@@ -192,7 +195,13 @@ export class OverlayNotificationsContentService
|
||||
{ transform: "translateX(0)", opacity: "1" },
|
||||
true,
|
||||
);
|
||||
setElementStyles(this.notificationBarElement, { boxShadow: "2px 4px 6px 0px #0000001A" }, true);
|
||||
if (!this.notificationRefreshFlag) {
|
||||
setElementStyles(
|
||||
this.notificationBarElement,
|
||||
{ boxShadow: "2px 4px 6px 0px #0000001A" },
|
||||
true,
|
||||
);
|
||||
}
|
||||
this.notificationBarIframeElement.removeEventListener(
|
||||
EVENTS.LOAD,
|
||||
this.handleNotificationBarIframeOnLoad,
|
||||
@@ -206,7 +215,13 @@ export class OverlayNotificationsContentService
|
||||
if (this.notificationBarIframeElement) {
|
||||
this.notificationBarElement = globalThis.document.createElement("div");
|
||||
this.notificationBarElement.id = "bit-notification-bar";
|
||||
|
||||
setElementStyles(this.notificationBarElement, this.notificationBarElementStyles, true);
|
||||
|
||||
if (this.notificationRefreshFlag) {
|
||||
setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true);
|
||||
}
|
||||
|
||||
this.notificationBarElement.appendChild(this.notificationBarIframeElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ describe("AutofillService", () => {
|
||||
const autofillOverlayMenuBootstrapScript = "bootstrap-autofill-overlay-menu.js";
|
||||
const autofillOverlayNotificationsBootstrapScript =
|
||||
"bootstrap-autofill-overlay-notifications.js";
|
||||
const defaultAutofillScripts = ["autofiller.js", "notificationBar.js", "contextMenuHandler.js"];
|
||||
const defaultAutofillScripts = ["autofiller.js", "contextMenuHandler.js"];
|
||||
const defaultExecuteScriptOptions = { runAt: "document_start" };
|
||||
let tabMock: chrome.tabs.Tab;
|
||||
let sender: chrome.runtime.MessageSender;
|
||||
@@ -400,13 +400,9 @@ describe("AutofillService", () => {
|
||||
});
|
||||
|
||||
it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => {
|
||||
configService.getFeatureFlag.mockImplementation(async (_feature) => {
|
||||
if (_feature === FeatureFlag.NotificationBarAddLoginImprovements) {
|
||||
return false as FeatureFlagValueType<any>;
|
||||
}
|
||||
enableChangedPasswordPromptMock$.next(false);
|
||||
enableAddedLoginPromptMock$.next(false);
|
||||
|
||||
return true as FeatureFlagValueType<any>;
|
||||
});
|
||||
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true);
|
||||
|
||||
[autofillOverlayMenuBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => {
|
||||
@@ -457,25 +453,12 @@ describe("AutofillService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("will inject the bootstrap-autofill script if the user does not have the autofill overlay enabled", async () => {
|
||||
it("will inject the overlay script if the user does not have the autofill overlay enabled", async () => {
|
||||
jest
|
||||
.spyOn(autofillService, "getInlineMenuVisibility")
|
||||
.mockResolvedValue(AutofillOverlayVisibility.Off);
|
||||
configService.getFeatureFlag.mockImplementation(async (_feature) => {
|
||||
if (_feature === FeatureFlag.NotificationBarAddLoginImprovements) {
|
||||
return false as FeatureFlagValueType<any>;
|
||||
}
|
||||
|
||||
return true as FeatureFlagValueType<any>;
|
||||
});
|
||||
|
||||
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||
|
||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||
file: `content/${autofillBootstrapScript}`,
|
||||
frameId: sender.frameId,
|
||||
...defaultExecuteScriptOptions,
|
||||
});
|
||||
expect(BrowserApi.executeScriptInTab).not.toHaveBeenCalledWith(tabMock.id, {
|
||||
file: `content/${autofillOverlayBootstrapScript}`,
|
||||
frameId: sender.frameId,
|
||||
|
||||
@@ -236,13 +236,8 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
const accountIsUnlocked = authStatus === AuthenticationStatus.Unlocked;
|
||||
let autoFillOnPageLoadIsEnabled = false;
|
||||
const addLoginImprovementsFlagActive = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.NotificationBarAddLoginImprovements,
|
||||
);
|
||||
|
||||
const injectedScripts = [
|
||||
await this.getBootstrapAutofillContentScript(activeAccount, addLoginImprovementsFlagActive),
|
||||
];
|
||||
const injectedScripts = [await this.getBootstrapAutofillContentScript(activeAccount)];
|
||||
|
||||
if (activeAccount && accountIsUnlocked) {
|
||||
autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad();
|
||||
@@ -259,10 +254,6 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
});
|
||||
}
|
||||
|
||||
if (!addLoginImprovementsFlagActive) {
|
||||
injectedScripts.push("notificationBar.js");
|
||||
}
|
||||
|
||||
injectedScripts.push("contextMenuHandler.js");
|
||||
|
||||
for (const injectedScript of injectedScripts) {
|
||||
@@ -283,11 +274,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
* enabled.
|
||||
*
|
||||
* @param activeAccount - The active account
|
||||
* @param addLoginImprovementsFlagActive - Whether the add login improvements feature flag is active
|
||||
*/
|
||||
private async getBootstrapAutofillContentScript(
|
||||
activeAccount: { id: UserId | undefined } & AccountInfo,
|
||||
addLoginImprovementsFlagActive = false,
|
||||
): Promise<string> {
|
||||
let inlineMenuVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off;
|
||||
|
||||
@@ -310,8 +299,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
const enableAddedLoginPrompt = await firstValueFrom(
|
||||
this.userNotificationSettingsService.enableAddedLoginPrompt$,
|
||||
);
|
||||
const isNotificationBarEnabled =
|
||||
addLoginImprovementsFlagActive && (enableChangedPasswordPrompt || enableAddedLoginPrompt);
|
||||
const isNotificationBarEnabled = enableChangedPasswordPrompt || enableAddedLoginPrompt;
|
||||
|
||||
if (!inlineMenuVisibility && !isNotificationBarEnabled) {
|
||||
return "bootstrap-autofill.js";
|
||||
|
||||
@@ -1195,7 +1195,6 @@ export default class MainBackground {
|
||||
|
||||
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
|
||||
this.logService,
|
||||
this.configService,
|
||||
this.notificationBackground,
|
||||
);
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import { RemovePasswordComponent } from "../auth/popup/remove-password.component
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
|
||||
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||
@@ -89,7 +88,6 @@ import "../platform/popup/locales";
|
||||
ColorPasswordPipe,
|
||||
ColorPasswordCountPipe,
|
||||
SetPasswordComponent,
|
||||
SsoComponentV1,
|
||||
TabsV2Component,
|
||||
UpdateTempPasswordComponent,
|
||||
UserVerificationComponent,
|
||||
|
||||
@@ -201,7 +201,6 @@ const mainConfig = {
|
||||
"./src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts",
|
||||
"content/autofiller": "./src/autofill/content/autofiller.ts",
|
||||
"content/auto-submit-login": "./src/autofill/content/auto-submit-login.ts",
|
||||
"content/notificationBar": "./src/autofill/content/notification-bar.ts",
|
||||
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
|
||||
"content/content-message-handler": "./src/autofill/content/content-message-handler.ts",
|
||||
"content/fido2-content-script": "./src/autofill/fido2/content/fido2-content-script.ts",
|
||||
|
||||
@@ -15,7 +15,6 @@ import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { LoginModule } from "../auth/login/login.module";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { SsoComponentV1 } from "../auth/sso-v1.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
@@ -76,7 +75,6 @@ import { SharedModule } from "./shared/shared.module";
|
||||
SetPasswordComponent,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
SsoComponentV1,
|
||||
UpdateTempPasswordComponent,
|
||||
VaultComponent,
|
||||
VaultTimeoutInputComponent,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<form id="sso-page" (ngSubmit)="submit()">
|
||||
<div class="content">
|
||||
<img class="logo-image" alt="Bitwarden" />
|
||||
<div class="box">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@Component({
|
||||
selector: "app-sso",
|
||||
templateUrl: "sso-v1.component.html",
|
||||
})
|
||||
export class SsoComponentV1 extends BaseSsoComponent {
|
||||
constructor(
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
syncService: SyncService,
|
||||
route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
logService: LogService,
|
||||
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
configService: ConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
ssoLoginService,
|
||||
loginStrategyService,
|
||||
router,
|
||||
i18nService,
|
||||
route,
|
||||
stateService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
logService,
|
||||
userDecryptionOptionsService,
|
||||
configService,
|
||||
masterPasswordService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
this.onSuccessfulLogin = async () => {
|
||||
// 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
|
||||
syncService.fullSync(true);
|
||||
};
|
||||
|
||||
this.onSuccessfulLoginTde = async () => {
|
||||
// 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
|
||||
syncService.fullSync(true);
|
||||
};
|
||||
|
||||
this.redirectUri = "bitwarden://sso-callback";
|
||||
this.clientId = "desktop";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
@import "variables.scss";
|
||||
|
||||
#lock-page,
|
||||
#sso-page,
|
||||
#set-password-page,
|
||||
#remove-password-page {
|
||||
display: flex;
|
||||
@@ -106,18 +105,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
#sso-page {
|
||||
.content {
|
||||
width: 325px;
|
||||
|
||||
.box {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#set-password-page,
|
||||
#remove-password-page {
|
||||
.content {
|
||||
|
||||
@@ -1,18 +1,83 @@
|
||||
FROM ghcr.io/bitwarden/server
|
||||
###############################################
|
||||
# Build stage 1 #
|
||||
###############################################
|
||||
ARG NODE_VERSION=20
|
||||
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS node-build
|
||||
|
||||
ARG NPM_COMMAND=dist:bit:selfhost
|
||||
|
||||
WORKDIR /source
|
||||
COPY . .
|
||||
|
||||
RUN npm ci
|
||||
|
||||
WORKDIR /source/apps/web
|
||||
RUN npm run ${NPM_COMMAND}
|
||||
|
||||
###############################################
|
||||
# Build stage 2 #
|
||||
###############################################
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
|
||||
# Docker buildx supplies the value for this arg
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Determine proper runtime value for .NET
|
||||
# We put the value in a file to be read by later layers.
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
RID=linux-x64 ; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
RID=linux-arm64 ; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||
RID=linux-arm ; \
|
||||
fi \
|
||||
&& echo "RID=$RID" > /tmp/rid.txt
|
||||
|
||||
# Copy csproj files as distinct layers
|
||||
WORKDIR /source
|
||||
COPY server/util/Server/*.csproj ./util/Server/
|
||||
COPY server/Directory.Build.props .
|
||||
COPY server/.editorconfig .
|
||||
|
||||
# Restore Server project dependencies and tools
|
||||
WORKDIR /source/util/Server
|
||||
RUN . /tmp/rid.txt && dotnet restore -r $RID
|
||||
|
||||
# Copy required project files
|
||||
WORKDIR /source
|
||||
COPY server/util/Server/. ./util/Server/
|
||||
COPY server/.git/. ./.git/
|
||||
|
||||
# Build Server app
|
||||
WORKDIR /source/util/Server
|
||||
RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Server --no-restore --no-self-contained -r $RID
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
###############################################
|
||||
# App stage #
|
||||
###############################################
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
LABEL com.bitwarden.product="bitwarden"
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
EXPOSE 5000
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy app from the build stage
|
||||
WORKDIR /bitwarden_server
|
||||
COPY --from=build /app/Server ./
|
||||
|
||||
ENV ASPNETCORE_URLS http://+:5000
|
||||
WORKDIR /app
|
||||
EXPOSE 5000
|
||||
COPY ./build .
|
||||
COPY entrypoint.sh /
|
||||
COPY --from=node-build /source/apps/web/build .
|
||||
COPY --from=node-build /source/apps/web/entrypoint.sh /
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1
|
||||
|
||||
@@ -111,10 +111,10 @@
|
||||
|
||||
<ng-container *ngIf="loaded && usePlaceHolderEvents">
|
||||
<div
|
||||
class="tw-relative tw--top-72 tw-bg-[#ffffff] tw-bg-opacity-90 tw-pb-5 tw-flex tw-items-center tw-justify-center tw-h-[19rem]"
|
||||
class="tw-relative tw--top-72 tw-bg-background tw-bg-opacity-90 tw-pb-5 tw-flex tw-items-center tw-justify-center tw-h-[19rem]"
|
||||
>
|
||||
<div
|
||||
class="tw-bg-[#ffffff] tw-max-w-xl tw-flex-col tw-justify-center tw-text-center tw-p-5 tw-px-10 tw-rounded tw-border-0 tw-border-b tw-border-secondary-300 tw-border-solid tw-mt-5"
|
||||
class="tw-bg-background tw-max-w-xl tw-flex-col tw-justify-center tw-text-center tw-p-5 tw-px-10 tw-rounded tw-border-0 tw-border-b tw-border-secondary-300 tw-border-solid tw-mt-5"
|
||||
>
|
||||
<i class="bwi bwi-2x bwi-business tw-text-primary-600"></i>
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access-attachments",
|
||||
templateUrl: "../../../../vault/individual-vault/attachments.component.html",
|
||||
})
|
||||
export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponent {
|
||||
viewOnly = true;
|
||||
canAccessAttachments = true;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
encryptService: EncryptService,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
i18nService,
|
||||
keyService,
|
||||
encryptService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
window,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
// Do nothing since cipher is already decoded
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(attachment: AttachmentView) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -38,28 +38,6 @@
|
||||
<br />
|
||||
<small class="tw-text-xs">{{ currentCipher.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<div *ngIf="currentCipher.hasAttachments">
|
||||
<button
|
||||
[bitMenuTriggerFor]="optionsMenu"
|
||||
type="button"
|
||||
buttonType="main"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #optionsMenu>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
appStopClick
|
||||
(click)="viewAttachments(currentCipher)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
|
||||
{{ "attachments" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { EmergencyAccessId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwarden/vault";
|
||||
|
||||
import { EmergencyAccessService } from "../../../emergency-access";
|
||||
import { EmergencyAccessAttachmentsComponent } from "../attachments/emergency-access-attachments.component";
|
||||
|
||||
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
|
||||
|
||||
@@ -20,56 +18,34 @@ import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"
|
||||
})
|
||||
export class EmergencyAccessViewComponent implements OnInit {
|
||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||
attachmentsModalRef: ViewContainerRef;
|
||||
|
||||
id: string;
|
||||
id: EmergencyAccessId | null = null;
|
||||
ciphers: CipherView[] = [];
|
||||
loaded = false;
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private emergencyAccessService: EmergencyAccessService,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.route.params.subscribe((qParams) => {
|
||||
if (qParams.id == null) {
|
||||
return this.router.navigate(["settings/emergency-access"]);
|
||||
}
|
||||
async ngOnInit() {
|
||||
const qParams = await firstValueFrom(this.route.params);
|
||||
if (qParams.id == null) {
|
||||
await this.router.navigate(["settings/emergency-access"]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = qParams.id;
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.load();
|
||||
});
|
||||
this.id = qParams.id;
|
||||
this.ciphers = await this.emergencyAccessService.getViewOnlyCiphers(qParams.id);
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async selectCipher(cipher: CipherView) {
|
||||
EmergencyViewDialogComponent.open(this.dialogService, {
|
||||
cipher,
|
||||
emergencyAccessId: this.id!,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.ciphers = await this.emergencyAccessService.getViewOnlyCiphers(this.id);
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
// FIXME PM-17747: This will also need to be replaced with the new AttachmentViewDialog
|
||||
async viewAttachments(cipher: CipherView) {
|
||||
await this.modalService.openViewRef(
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
this.attachmentsModalRef,
|
||||
(comp) => {
|
||||
comp.cipher = cipher;
|
||||
comp.emergencyAccessId = this.id;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{{ title }}
|
||||
</span>
|
||||
<div bitDialogContent #dialogContent>
|
||||
<app-cipher-view [cipher]="cipher"></app-cipher-view>
|
||||
<app-cipher-view [emergencyAccessId]="emergencyAccessId" [cipher]="cipher"></app-cipher-view>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="cancel()">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { EmergencyAccessId } from "@bitwarden/common/types/guid";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -20,6 +21,7 @@ import { WebViewPasswordHistoryService } from "../../../../vault/services/web-vi
|
||||
export interface EmergencyViewDialogParams {
|
||||
/** The cipher being viewed. */
|
||||
cipher: CipherView;
|
||||
emergencyAccessId: EmergencyAccessId;
|
||||
}
|
||||
|
||||
/** Stubbed class, premium upgrade is not applicable for emergency viewing */
|
||||
@@ -59,6 +61,10 @@ export class EmergencyViewDialogComponent {
|
||||
return this.params.cipher;
|
||||
}
|
||||
|
||||
get emergencyAccessId(): EmergencyAccessId {
|
||||
return this.params.emergencyAccessId;
|
||||
}
|
||||
|
||||
cancel = () => {
|
||||
this.dialogRef.close();
|
||||
};
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit" class="tw-container">
|
||||
<div *ngIf="loggingIn">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
<div *ngIf="!loggingIn">
|
||||
<p bitTypography="body1">{{ "ssoLogInWithOrgIdentifier" | i18n }}</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "ssoIdentifier" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="identifier" appAutofocus />
|
||||
</bit-form-field>
|
||||
<hr />
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [block]="true">
|
||||
{{ "logIn" | i18n }}
|
||||
</button>
|
||||
<a bitButton buttonType="secondary" routerLink="/login" [block]="true">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,186 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction";
|
||||
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
|
||||
import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@Component({
|
||||
selector: "app-sso",
|
||||
templateUrl: "sso-v1.component.html",
|
||||
})
|
||||
export class SsoComponentV1 extends BaseSsoComponent implements OnInit {
|
||||
protected formGroup = new FormGroup({
|
||||
identifier: new FormControl(null, [Validators.required]),
|
||||
});
|
||||
|
||||
get identifierFormControl() {
|
||||
return this.formGroup.controls.identifier;
|
||||
}
|
||||
|
||||
constructor(
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
logService: LogService,
|
||||
private orgDomainApiService: OrgDomainApiServiceAbstraction,
|
||||
private validationService: ValidationService,
|
||||
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
configService: ConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
ssoLoginService,
|
||||
loginStrategyService,
|
||||
router,
|
||||
i18nService,
|
||||
route,
|
||||
stateService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
logService,
|
||||
userDecryptionOptionsService,
|
||||
configService,
|
||||
masterPasswordService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
this.redirectUri = window.location.origin + "/sso-connector.html";
|
||||
this.clientId = "web";
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// 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
|
||||
super.ngOnInit();
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
if (qParams.identifier != null) {
|
||||
// SSO Org Identifier in query params takes precedence over claimed domains
|
||||
this.identifierFormControl.setValue(qParams.identifier);
|
||||
this.loggingIn = true;
|
||||
await this.submit();
|
||||
} else {
|
||||
// Note: this flow is written for web but both browser and desktop
|
||||
// redirect here on SSO button click.
|
||||
|
||||
// Check if email matches any claimed domains
|
||||
if (qParams.email) {
|
||||
// show loading spinner
|
||||
this.loggingIn = true;
|
||||
try {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.VerifiedSsoDomainEndpoint)) {
|
||||
const response: ListResponse<VerifiedOrganizationDomainSsoDetailsResponse> =
|
||||
await this.orgDomainApiService.getVerifiedOrgDomainsByEmail(qParams.email);
|
||||
|
||||
if (response.data.length > 0) {
|
||||
this.identifierFormControl.setValue(response.data[0].organizationIdentifier);
|
||||
await this.submit();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const response: OrganizationDomainSsoDetailsResponse =
|
||||
await this.orgDomainApiService.getClaimedOrgDomainByEmail(qParams.email);
|
||||
|
||||
if (response?.ssoAvailable && response?.verifiedDate) {
|
||||
this.identifierFormControl.setValue(response.organizationIdentifier);
|
||||
await this.submit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleGetClaimedDomainByEmailError(error);
|
||||
}
|
||||
|
||||
this.loggingIn = false;
|
||||
}
|
||||
|
||||
// Fallback to state svc if domain is unclaimed
|
||||
const storedIdentifier = await this.ssoLoginService.getOrganizationSsoIdentifier();
|
||||
if (storedIdentifier != null) {
|
||||
this.identifierFormControl.setValue(storedIdentifier);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleGetClaimedDomainByEmailError(error: any): void {
|
||||
if (error instanceof ErrorResponse) {
|
||||
const errorResponse: ErrorResponse = error as ErrorResponse;
|
||||
switch (errorResponse.statusCode) {
|
||||
case HttpStatusCode.NotFound:
|
||||
//this is a valid case for a domain not found
|
||||
return;
|
||||
|
||||
default:
|
||||
this.validationService.showError(errorResponse);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const autoSubmit = (await firstValueFrom(this.route.queryParams)).identifier != null;
|
||||
|
||||
this.identifier = this.identifierFormControl.value;
|
||||
await this.ssoLoginService.setOrganizationSsoIdentifier(this.identifier);
|
||||
if (this.clientId === "browser") {
|
||||
document.cookie = `ssoHandOffMessage=${this.i18nService.t("ssoHandOff")};SameSite=strict`;
|
||||
}
|
||||
try {
|
||||
await Object.getPrototypeOf(this).submit.call(this);
|
||||
} catch (error) {
|
||||
if (autoSubmit) {
|
||||
await this.router.navigate(["/login"]);
|
||||
} else {
|
||||
this.validationService.showError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import { DangerZoneComponent } from "../auth/settings/account/danger-zone.compon
|
||||
import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component";
|
||||
import { DeleteAccountDialogComponent } from "../auth/settings/account/delete-account-dialog.component";
|
||||
import { ProfileComponent } from "../auth/settings/account/profile.component";
|
||||
import { EmergencyAccessAttachmentsComponent } from "../auth/settings/emergency-access/attachments/emergency-access-attachments.component";
|
||||
import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/confirm/emergency-access-confirm.component";
|
||||
import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component";
|
||||
import { EmergencyAccessComponent } from "../auth/settings/emergency-access/emergency-access.component";
|
||||
@@ -42,7 +41,6 @@ import { TwoFactorSetupYubiKeyComponent } from "../auth/settings/two-factor/two-
|
||||
import { TwoFactorSetupComponent } from "../auth/settings/two-factor/two-factor-setup.component";
|
||||
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor/two-factor-verify.component";
|
||||
import { UserVerificationModule } from "../auth/shared/components/user-verification";
|
||||
import { SsoComponentV1 } from "../auth/sso-v1.component";
|
||||
import { UpdatePasswordComponent } from "../auth/update-password.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
|
||||
@@ -116,7 +114,6 @@ import { SharedModule } from "./shared.module";
|
||||
DeleteAccountDialogComponent,
|
||||
DomainRulesComponent,
|
||||
EmergencyAccessAddEditComponent,
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
EmergencyAccessComponent,
|
||||
EmergencyAccessConfirmComponent,
|
||||
EmergencyAccessTakeoverComponent,
|
||||
@@ -146,7 +143,6 @@ import { SharedModule } from "./shared.module";
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
SsoComponentV1,
|
||||
TwoFactorSetupAuthenticatorComponent,
|
||||
TwoFactorSetupDuoComponent,
|
||||
TwoFactorSetupEmailComponent,
|
||||
@@ -175,7 +171,6 @@ import { SharedModule } from "./shared.module";
|
||||
DomainRulesComponent,
|
||||
DynamicAvatarComponent,
|
||||
EmergencyAccessAddEditComponent,
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
EmergencyAccessComponent,
|
||||
EmergencyAccessConfirmComponent,
|
||||
EmergencyAccessTakeoverComponent,
|
||||
@@ -206,7 +201,6 @@ import { SharedModule } from "./shared.module";
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
SsoComponentV1,
|
||||
TwoFactorSetupAuthenticatorComponent,
|
||||
TwoFactorSetupDuoComponent,
|
||||
TwoFactorSetupEmailComponent,
|
||||
|
||||
@@ -25,7 +25,6 @@ export enum FeatureFlag {
|
||||
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
|
||||
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
||||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||
NotificationRefresh = "notification-refresh",
|
||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||
MacOsNativeCredentialSync = "macos-native-credential-sync",
|
||||
@@ -87,7 +86,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
||||
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
||||
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
|
||||
[FeatureFlag.NotificationRefresh]: FALSE,
|
||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||
|
||||
@@ -12,3 +12,4 @@ export type SendId = Opaque<string, "SendId">;
|
||||
export type IndexedEntityId = Opaque<string, "IndexedEntityId">;
|
||||
export type SecurityTaskId = Opaque<string, "SecurityTaskId">;
|
||||
export type NotificationId = Opaque<string, "NotificationId">;
|
||||
export type EmergencyAccessId = Opaque<string, "EmergencyAccessId">;
|
||||
|
||||
@@ -234,6 +234,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.organizationId = value !== "myVault" ? value : undefined;
|
||||
|
||||
this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip");
|
||||
this.exportForm.get("format").setValue("json");
|
||||
if (value === "myVault" && isExportAttachmentsEnabled) {
|
||||
this.formatOptions.push({ name: ".zip (with attachments)", value: "zip" });
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[cipher]="cipher"
|
||||
[attachment]="attachment"
|
||||
[checkPwReprompt]="true"
|
||||
[emergencyAccessId]="emergencyAccessId"
|
||||
></app-download-attachment>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
@@ -41,6 +41,9 @@ import { DownloadAttachmentComponent } from "../../components/download-attachmen
|
||||
export class AttachmentsV2ViewComponent {
|
||||
@Input() cipher: CipherView;
|
||||
|
||||
// Required for fetching attachment data when viewed from cipher via emergency access
|
||||
@Input() emergencyAccessId?: EmergencyAccessId;
|
||||
|
||||
canAccessPremium: boolean;
|
||||
orgKey: OrgKey;
|
||||
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
|
||||
<!-- ATTACHMENTS SECTION -->
|
||||
<ng-container *ngIf="cipher.attachments">
|
||||
<app-attachments-v2-view [cipher]="cipher"> </app-attachments-v2-view>
|
||||
<app-attachments-v2-view [emergencyAccessId]="emergencyAccessId" [cipher]="cipher">
|
||||
</app-attachments-v2-view>
|
||||
</ng-container>
|
||||
|
||||
<!-- ITEM HISTORY SECTION -->
|
||||
|
||||
@@ -15,7 +15,7 @@ import { isCardExpired } from "@bitwarden/common/autofill/utils";
|
||||
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 { CipherId, CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, CollectionId, EmergencyAccessId, 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";
|
||||
@@ -61,6 +61,9 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
|
||||
export class CipherViewComponent implements OnChanges, OnDestroy {
|
||||
@Input({ required: true }) cipher: CipherView | null = null;
|
||||
|
||||
// Required for fetching attachment data when viewed from cipher via emergency access
|
||||
@Input() emergencyAccessId?: EmergencyAccessId;
|
||||
|
||||
activeUserId$ = getUserId(this.accountService.activeAccount$);
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -36,6 +36,9 @@ export class DownloadAttachmentComponent {
|
||||
// When in view mode, we will want to check for the master password reprompt
|
||||
@Input() checkPwReprompt?: boolean = false;
|
||||
|
||||
// Required for fetching attachment data when viewed from cipher via emergency access
|
||||
@Input() emergencyAccessId?: EmergencyAccessId;
|
||||
|
||||
/** The organization key if the cipher is associated with one */
|
||||
private orgKey: OrgKey | null = null;
|
||||
|
||||
@@ -68,6 +71,7 @@ export class DownloadAttachmentComponent {
|
||||
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
|
||||
this.cipher.id,
|
||||
this.attachment.id,
|
||||
this.emergencyAccessId,
|
||||
);
|
||||
url = attachmentDownloadResponse.url;
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user