1
0
mirror of https://github.com/bitwarden/web synced 2025-12-10 05:13:40 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Hinton
1136942dd3 Move files 2022-01-31 08:58:44 +01:00
Hinton
5d58c18996 Merge branch 'master' of github.com:bitwarden/web into feature/angular-cli 2022-01-29 02:07:08 +01:00
Hinton
17f5cf74b5 WIP angular cli 2022-01-25 20:06:29 +01:00
682 changed files with 48251 additions and 33660 deletions

17
.browserslistrc Normal file
View File

@@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@@ -1,8 +0,0 @@
**/dist
**/build
jslib
webpack.config.js
scripts/optimize.js
config.js
**/node_modules

View File

@@ -1,31 +0,0 @@
{
"root": true,
"env": {
"browser": true
},
"extends": ["./jslib/shared/eslintrc.json"],
"rules": {
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always",
"pathGroups": [
{
"pattern": "jslib-*/**",
"group": "external",
"position": "after"
},
{
"pattern": "src/**/*",
"group": "parent",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"]
}
]
}
}

View File

@@ -21,6 +21,10 @@
<!--Required for any UI changes. Delete if not applicable-->
## Testing requirements
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required)

View File

@@ -11,10 +11,6 @@ on:
branches-ignore:
- "l10n_master"
- "gh-pages"
- "deploy"
paths-ignore:
- '.github/workflows/**'
jobs:
cloc:
@@ -32,28 +28,6 @@ jobs:
- name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
lint:
name: Lint
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-lint-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
setup:
name: Setup
runs-on: ubuntu-20.04
@@ -67,26 +41,25 @@ jobs:
id: version
run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
build-oss-selfhost:
name: Build OSS zip
runs-on: ubuntu-20.04
needs:
- setup
- lint
needs: setup
env:
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: "16"
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment
run: |
whoami
@@ -97,6 +70,9 @@ jobs:
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Install dependencies
run: npm ci
@@ -112,26 +88,25 @@ jobs:
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
if-no-files-found: error
build-cloud:
name: Build Cloud zip
runs-on: ubuntu-20.04
needs:
- setup
- lint
needs: setup
env:
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: "16"
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment
run: |
whoami
@@ -142,6 +117,9 @@ jobs:
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Install dependencies
run: npm ci
@@ -157,26 +135,25 @@ jobs:
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
if-no-files-found: error
build-commercial-selfhost:
name: Build SelfHost Docker image
runs-on: ubuntu-20.04
needs:
- setup
- lint
needs: setup
env:
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: "16"
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment
run: |
whoami
@@ -188,13 +165,19 @@ jobs:
echo "GitHub event: $GITHUB_EVENT"
- name: Setup DCT
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
id: setup-dct
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
with:
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
azure-keyvault-name: "bitwarden-prod-kv"
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Restore
run: dotnet tool restore
- name: Install dependencies
run: npm ci
@@ -229,11 +212,11 @@ jobs:
run: docker tag bitwarden/web bitwarden/web:dev
- name: Tag hotfix branch
if: github.ref == 'refs/heads/hotfix-rc'
run: docker tag bitwarden/web bitwarden/web:hotfix-rc
if: github.ref == 'refs/heads/hotfix'
run: docker tag bitwarden/web bitwarden/web:hotfix
- name: List Docker images
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
run: docker images
- name: Push rc image
@@ -251,59 +234,32 @@ jobs:
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
- name: Push hotfix image
if: github.ref == 'refs/heads/hotfix-rc'
run: docker push bitwarden/web:hotfix-rc
if: github.ref == 'refs/heads/hotfix'
run: docker push bitwarden/web:hotfix
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
- name: Log out of Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
run: |
docker logout
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
- name: Login to Azure - QA Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
- name: Login to Azure ACR
run: az acr login -n bitwardenqa
- name: Tag and Push RC to Azure ACR QA registry
env:
REGISTRY: bitwardenqa.azurecr.io
run: |
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
if [[ "$IMAGE_TAG" == "master" ]]; then
IMAGE_TAG=dev
fi
docker tag bitwarden/web \
$REGISTRY/web-sh:$IMAGE_TAG
docker push $REGISTRY/web-sh:$IMAGE_TAG
- name: Log out of Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
run: docker logout
build-qa:
name: Build Docker images for QA environment
runs-on: ubuntu-20.04
needs:
- setup
- lint
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: "16"
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment
run: |
whoami
@@ -322,6 +278,12 @@ jobs:
- name: Log into container registry
run: az acr login -n bitwardenqa
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Restore
run: dotnet tool restore
- name: Install dependencies
run: npm ci
@@ -377,29 +339,35 @@ jobs:
- name: Log out of Docker
run: docker logout
windows:
name: Test code on Windows
runs-on: windows-2019
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up NuGet
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
with:
nuget-version: "latest"
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
- name: Set up MSBuild
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: "16"
- name: Print environment
run: |
nuget help | grep Version
msbuild -version
dotnet --info
node --version
npm --version
echo "GitHub ref: $GITHUB_REF"
@@ -408,13 +376,18 @@ jobs:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: NPM build
run: npm run build:bit:cloud
crowdin-push:
name: Crowdin Push
if: github.ref == 'refs/heads/master'
@@ -453,7 +426,6 @@ jobs:
upload_sources: true
upload_translations: false
check-failures:
name: Check for failures
if: always()
@@ -461,7 +433,6 @@ jobs:
needs:
- cloc
- setup
- lint
- build-oss-selfhost
- build-cloud
- build-commercial-selfhost
@@ -473,7 +444,6 @@ jobs:
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
env:
CLOC_STATUS: ${{ needs.cloc.result }}
LINT_STATUS: ${{ needs.lint.result }}
SETUP_STATUS: ${{ needs.setup.result }}
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
@@ -484,8 +454,6 @@ jobs:
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$LINT_STATUS" = "failure" ]; then
exit 1
elif [ "$SETUP_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then

View File

@@ -1,16 +0,0 @@
---
name: Enforce PR labels
on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
jobs:
enforce-label:
name: EnforceLabel
runs-on: ubuntu-20.04
steps:
- name: Enforce Label
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
with:
BANNED_LABELS: "hold"
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"

View File

@@ -12,7 +12,6 @@ on:
options:
- Initial Release
- Redeploy
- Dry Run
jobs:
setup:
@@ -21,20 +20,19 @@ jobs:
outputs:
release_version: ${{ steps.version.outputs.package }}
tag_version: ${{ steps.version.outputs.tag }}
branch_name: ${{ steps.branch.outputs.branch_name }}
branch-name: ${{ steps.branch.outputs.branch-name }}
steps:
- name: Branch check
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then
echo "==================================="
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
echo "[!] Can only release from the 'rc' or 'hotfix' branches"
echo "==================================="
exit 1
fi
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # 2.4.0
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4
- name: Check Release Version
id: version
@@ -57,17 +55,15 @@ jobs:
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch_name::$BRANCH_NAME"
echo "::set-output name=branch-name::$BRANCH_NAME"
self-host:
name: Release self-host docker
runs-on: ubuntu-20.04
needs: setup
env:
_BRANCH_NAME: ${{ needs.setup.outputs.branch_name }}
_BRANCH_NAME: ${{ needs.setup.outputs.branch-name }}
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_RELEASE_OPTION: ${{ github.event.inputs.release_type }}
steps:
- name: Print environment
run: |
@@ -75,12 +71,7 @@ jobs:
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
echo "Github Release Option: $_RELEASE_OPTION"
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
########## DockerHub ##########
- name: Setup DCT
id: setup-dct
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
@@ -88,25 +79,21 @@ jobs:
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
azure-keyvault-name: "bitwarden-prod-kv"
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Pull latest selfhost image
run: |
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
docker pull bitwarden/web:latest
else
docker pull bitwarden/web:$_BRANCH_NAME
fi
run: docker pull bitwarden/web:$_BRANCH_NAME
- name: Tag version and latest
run: |
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
docker tag bitwarden/web:latest bitwarden/web:dryrun
else
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest
fi
- name: List Docker images
run: docker images
- name: Push version and latest image
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
@@ -114,51 +101,11 @@ jobs:
docker push bitwarden/web:$_RELEASE_VERSION
docker push bitwarden/web:latest
- name: Log out of Docker and disable Docker Notary
run: |
docker logout
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
########## ACR ##########
- name: Login to Azure - QA Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
- name: Login to Azure ACR
run: az acr login -n bitwardenqa
- name: Tag version and latest
env:
REGISTRY: bitwardenqa.azurecr.io
run: |
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
docker tag bitwarden/web:latest $REGISTRY/web:dryrun
else
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web:latest
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web-sh:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web-sh:latest
fi
- name: Push version and latest image
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
env:
REGISTRY: bitwardenqa.azurecr.io
run: |
docker push $REGISTRY/web:$_RELEASE_VERSION
docker push $REGISTRY/web:latest
docker push $REGISTRY/web-sh:$_RELEASE_VERSION
docker push $REGISTRY/web-sh:latest
- name: Log out of Docker
run: docker logout
ghpages-deploy:
name: Deploy Web Vault to GitHub Pages
name: Deploy Web Vault
runs-on: ubuntu-20.04
needs:
- setup
@@ -168,17 +115,17 @@ jobs:
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
steps:
- name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
with:
ref: gh-pages
- name: Create gh-pages-deploy branch
- name: Create deploy branch
run: |
git switch -c gh-pages-deploy-$_TAG_VERSION
git push -u origin gh-pages-deploy-$_TAG_VERSION
git switch -c deploy-$_TAG_VERSION
git push -u origin deploy-$_TAG_VERSION
- name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup git config
run: |
@@ -188,11 +135,11 @@ jobs:
git config --global url."https://".insteadOf ssh://
- name: Download latest cloud asset
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch_name }}
branch: ${{ needs.setup.outputs.branch-name }}
artifacts: web-*-cloud-COMMERCIAL.zip
# This should result in a build directory in the current working directory
@@ -200,92 +147,25 @@ jobs:
run: unzip web-*-cloud-COMMERCIAL.zip
- name: Deploy GitHub Pages
uses: crazy-max/ghaction-github-pages@a117e4aa1fb4854d021546d2abdfac95be568a3a # v2.6.0
uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
target_branch: gh-pages-deploy-${{ needs.setup.outputs.tag_version }}
target_branch: deploy-${{ needs.setup.outputs.tag_version }}
build_dir: build
keep_history: true
commit_message: "Staging deploy ${{ needs.setup.outputs.release_version }}"
dry_run: ${{ github.event.inputs.release_type == 'Dry Run' }}
- name: Create GitHub Pages Deploy PR
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
- name: Create Deploy PR
env:
PR_BRANCH: gh-pages-deploy-${{ env._TAG_VERSION }}
PR_BRANCH: deploy-${{ env._TAG_VERSION }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create --title "Deploy $_RELEASE_VERSION to GitHub Pages" \
gh pr create --title "Deploy $_RELEASE_VERSION" \
--body "Deploying $_RELEASE_VERSION" \
--base gh-pages \
--head "$PR_BRANCH"
cfpages-deploy:
name: Deploy Web Vault to CloudFlare Pages branch
runs-on: ubuntu-20.04
needs:
- setup
- self-host
env:
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
steps:
- name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Download latest cloud asset
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch_name }}
artifacts: web-*-cloud-COMMERCIAL.zip
# This should result in a build directory in the current working directory
- name: Unzip build asset
run: unzip web-*-cloud-COMMERCIAL.zip
- name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
with:
ref: deploy
path: deployment
- name: Setup git config
run: |
git config --global user.name = "GitHub Action Bot"
git config --global user.email = "<>"
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
git config --global url."https://".insteadOf ssh://
- name: Deploy CloudFlare Pages
run: |
rm -rf ./*
cp -R ../build/* .
working-directory: deployment
- name: Create cf-pages-deploy branch
run: |
git switch -c cf-pages-deploy-$_TAG_VERSION
git add .
git commit -m "Staging deploy ${{ needs.setup.outputs.release_version }}"
git push -u origin cf-pages-deploy-$_TAG_VERSION
working-directory: deployment
- name: Create CloudFlare Pages Deploy PR
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
env:
PR_BRANCH: cf-pages-deploy-${{ env._TAG_VERSION }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create --title "Deploy $_RELEASE_VERSION to CloudFlare Pages" \
--body "Deploying $_RELEASE_VERSION" \
--base deploy \
--head "$PR_BRANCH"
release:
name: Create GitHub Release
runs-on: ubuntu-20.04
@@ -293,14 +173,13 @@ jobs:
- setup
- self-host
- ghpages-deploy
- cfpages-deploy
steps:
- name: Download latest build artifacts
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch_name }}
branch: ${{ needs.setup.outputs.branch-name }}
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
web-*-selfhosted-open-source.zip"
@@ -310,8 +189,7 @@ jobs:
mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip
- name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09
with:
name: "Version ${{ needs.setup.outputs.release_version }}"
commit: ${{ github.sha }}
@@ -321,23 +199,3 @@ jobs:
web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
dry-run:
name: Dry Run Cleanup
runs-on: ubuntu-20.04
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
env:
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
needs:
- setup
- release
steps:
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # 2.4.0
- name: Remove gh-pages-deploy branch
run: git push origin --delete gh-pages-deploy-$_TAG_VERSION
- name: Remove cf-pages-deploy branch
run: git push origin --delete cf-pages-deploy-$_TAG_VERSION

View File

@@ -1,11 +0,0 @@
---
name: Workflow Linter
on:
pull_request:
paths:
- .github/workflows/**
jobs:
call-workflow:
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master

1
.gitignore vendored
View File

@@ -13,3 +13,4 @@ dist/
build/
!dev-server.shared.pem
config/local.json
.angular/

0
.husky/pre-commit Executable file → Normal file
View File

View File

@@ -10,7 +10,7 @@ Here is how you can get involved:
- **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
- **Report a bug or submit a bugfix:** Use Github issues and pull requests
- **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
- **Help other users:** Go to the [Ask the Bitwarden Community category](https://community.bitwarden.com/c/support/) on the Community Forums
- **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
- **Translate:** See the localization (l10n) section below
## Contributor Agreement
@@ -31,6 +31,6 @@ We use a translation tool called [Crowdin](https://crowdin.com) to help manage o
If you are interested in helping translate the Bitwarden web vault into another language (or make a translation correction), please register an account at Crowdin and join our project here: https://crowdin.com/project/bitwarden-web
If the language that you are interested in translating is not already listed, create a new account on Crowdin, join the project, and contact the project owner (https://crowdin.com/profile/dwbit).
If the language that you are interested in translating is not already listed, create a new account on Crowdin, join the project, and contact the project owner (https://crowdin.com/profile/kspearrin).
You can read Crowdin's getting started guide for translators here: https://support.crowdin.com/crowdin-intro/

View File

@@ -1,4 +1,4 @@
FROM bitwarden/server
FROM bitwarden/server:dev
LABEL com.bitwarden.product="bitwarden"

View File

@@ -1,9 +1,3 @@
> **Repository Reorganization in Progress**
>
> We are currently migrating some projects over to a mono repository. For existing PR's we will be providing documentation on how to move/migrate them. To minimize the overhead we are actively reviewing open PRs. If possible please ensure any pending comments are resolved as soon as possible.
>
> New pull requests created during this transition period may not get addressed —if needed, please create a new PR after the reorganization is complete.
<p align="center">
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/web-vault-macbook.png" alt="" width="600" height="358" />
</p>
@@ -67,10 +61,6 @@ You can also manually adjusting your API endpoint settings by adding `config/loc
Where the `urls` object is defined by the [Urls type in jslib](https://github.com/bitwarden/jslib/blob/master/common/src/abstractions/environment.service.ts).
## We're Hiring!
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.

View File

@@ -1,11 +1,39 @@
Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance!
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
users safe. If you believe you've found a security issue in our product or service, we encourage you to
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
# Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder.
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder.
- If you would like to encrypt your report, please use the PGP key with long ID
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
# In-scope
- Security issues in any current release of Bitwarden. This includes the web vault, browser extension,
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
code is available at https://github.com/bitwarden.
# Exclusions
The following bug classes are out-of scope:
- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden),
or that we already know of. Note that some of our issue tracking is private.
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
upstream maintainer.
- Attacks requiring physical access to a user's device.
- Self-XSS
- Issues related to software or protocols not under Bitwarden's control
- Vulnerabilities in outdated versions of Bitwarden
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
While researching, we'd like to ask you to refrain from:
@@ -14,8 +42,4 @@ While researching, we'd like to ask you to refrain from:
- Social engineering (including phishing) of Bitwarden staff or contractors
- Any physical attempts against Bitwarden property or data centers
# We want to help you!
If you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.
Thank you for helping keep Bitwarden and our users safe!

310
angular.json Normal file
View File

@@ -0,0 +1,310 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"web-vault": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "projects/web-vault",
"sourceRoot": "projects/web-vault/src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "extra-webpack.config.ts"
},
"outputPath": "dist/web-vault",
"index": "projects/web-vault/src/index.html",
"main": "projects/web-vault/src/main.ts",
"polyfills": "projects/web-vault/src/polyfills.ts",
"tsConfig": "projects/web-vault/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"projects/web-vault/src/assets",
{ "glob": "favicon.ico", "input": "projects/web-vault-internal/src", "output": "" },
{ "glob": ".nojekyll", "input": "projects/web-vault-internal/src", "output": "" },
{ "glob": "manifest.json", "input": "projects/web-vault-internal/src", "output": "" },
{ "glob": "app-id.json", "input": "projects/web-vault-internal/src", "output": "" },
{ "glob": "404.html", "input": "projects/web-vault-internal/src", "output": "" },
{ "glob": "**/*", "input": "projects/web-vault-internal/src/404", "output": "404" },
{
"glob": "**/*",
"input": "projects/web-vault-internal/src/locales",
"output": "locales"
},
{
"glob": "**/*",
"input": "projects/web-vault-internal/src/images",
"output": "images"
},
{ "glob": "qrious.min.js", "input": "node_modules/qrious/dist", "output": "scripts" },
{
"glob": "dropin.js",
"input": "node_modules/braintree-web-drop-in/dist/browser",
"output": "scripts"
}
],
"styles": [
"projects/web-vault/src/scss/styles.scss",
{
"input": "projects/web-vault-internal/src/connectors/captcha.scss",
"inject": false
},
{
"input": "projects/web-vault-internal/src/connectors/captcha-mobile.scss",
"inject": false
},
{
"input": "projects/web-vault-internal/src/connectors/duo.scss",
"inject": false
},
{
"input": "projects/web-vault-internal/src/connectors/sso.scss",
"inject": false
},
{
"input": "projects/web-vault-internal/src/connectors/webauthn.scss",
"inject": false
}
],
"resourcesOutputPath": "resources",
"scripts": ["projects/web-vault-internal/src/theme.js"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "7mb",
"maximumError": "8mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "projects/web-vault/src/environments/environment.ts",
"with": "projects/web-vault/src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"ssl": true,
"proxyConfig": "proxy.config.js"
},
"configurations": {
"production": {
"browserTarget": "web-vault:build:production"
},
"development": {
"browserTarget": "web-vault:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "web-vault:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/web-vault/src/test.ts",
"polyfills": "projects/web-vault/src/polyfills.ts",
"tsConfig": "projects/web-vault/tsconfig.spec.json",
"karmaConfig": "projects/web-vault/karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": ["projects/web-vault/src/favicon.ico", "projects/web-vault/src/assets"],
"styles": ["projects/web-vault/src/styles.scss"],
"scripts": []
}
}
}
},
"oss-vault": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "projects/oss-vault",
"sourceRoot": "projects/oss-vault/src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/oss-vault",
"index": "projects/oss-vault/src/index.html",
"main": "projects/oss-vault/src/main.ts",
"polyfills": "projects/oss-vault/src/polyfills.ts",
"tsConfig": "projects/oss-vault/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": ["projects/oss-vault/src/favicon.ico", "projects/oss-vault/src/assets"],
"styles": ["projects/oss-vault/src/styles.scss"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "projects/oss-vault/src/environments/environment.ts",
"with": "projects/oss-vault/src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "oss-vault:build:production"
},
"development": {
"browserTarget": "oss-vault:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "oss-vault:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/oss-vault/src/test.ts",
"polyfills": "projects/oss-vault/src/polyfills.ts",
"tsConfig": "projects/oss-vault/tsconfig.spec.json",
"karmaConfig": "projects/oss-vault/karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": ["projects/oss-vault/src/favicon.ico", "projects/oss-vault/src/assets"],
"styles": ["projects/oss-vault/src/styles.scss"],
"scripts": []
}
}
}
},
"web": {
"projectType": "library",
"root": "projects/web",
"sourceRoot": "projects/web/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/web/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/web/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/web/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/web/src/test.ts",
"tsConfig": "projects/web/tsconfig.spec.json",
"karmaConfig": "projects/web/karma.conf.js"
}
}
}
},
"webs": {
"projectType": "library",
"root": "projects/webs",
"sourceRoot": "projects/webs/src",
"prefix": "bitwarden",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/webs/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/webs/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/webs/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/webs/src/test.ts",
"tsConfig": "projects/webs/tsconfig.spec.json",
"karmaConfig": "projects/webs/karma.conf.js"
}
}
}
}
},
"defaultProject": "web-vault"
}

View File

@@ -1,68 +0,0 @@
import { Directive, Input, OnInit, Self } from "@angular/core";
import { ControlValueAccessor, FormControl, NgControl, Validators } from "@angular/forms";
import { dirtyRequired } from "jslib-angular/validators/dirty.validator";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Directive()
export abstract class BaseCvaComponent implements ControlValueAccessor, OnInit {
get describedById() {
return this.showDescribedBy ? this.controlId + "Desc" : null;
}
get showDescribedBy() {
return this.helperText != null || this.controlDir.control.hasError("required");
}
get isRequired() {
return (
this.controlDir.control.hasValidator(Validators.required) ||
this.controlDir.control.hasValidator(dirtyRequired)
);
}
@Input() label: string;
@Input() controlId: string;
@Input() helperText: string;
internalControl = new FormControl("");
protected onChange: any;
protected onTouched: any;
constructor(@Self() public controlDir: NgControl) {
this.controlDir.valueAccessor = this;
}
ngOnInit() {
this.internalControl.valueChanges.subscribe(this.onValueChangesInternal);
}
onBlurInternal() {
this.onTouched();
}
// CVA interfaces
writeValue(value: string) {
this.internalControl.setValue(value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.internalControl.disable();
} else {
this.internalControl.enable();
}
}
protected onValueChangesInternal: any = (value: string) => this.onChange(value);
// End CVA interfaces
}

View File

@@ -1,16 +0,0 @@
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[formControl]="internalControl"
(blur)="onBlurInternal()"
/>
<label class="form-check-label" [attr.for]="controlId">{{ label }}</label>
</div>
<small *ngIf="showDescribedBy" [attr.id]="describedById" class="form-text text-muted">{{
helperText
}}</small>
</div>

View File

@@ -1,10 +0,0 @@
import { Component } from "@angular/core";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-checkbox",
templateUrl: "input-checkbox.component.html",
})
export class InputCheckboxComponent extends BaseCvaComponent {}

View File

@@ -1,26 +0,0 @@
<div class="form-group">
<label>{{ label }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="controlValue" />
<div class="input-group-append" *ngIf="showLaunch">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launchUri(controlValue)"
>
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
</button>
</div>
<div class="input-group-append" *ngIf="showCopy">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(controlValue)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>

View File

@@ -1,25 +0,0 @@
import { Component, Input } from "@angular/core";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-text-readonly",
templateUrl: "input-text-readonly.component.html",
})
export class InputTextReadOnlyComponent {
@Input() controlValue: string;
@Input() label: string;
@Input() showCopy = true;
@Input() showLaunch = false;
constructor(private platformUtilsService: PlatformUtilsService) {}
copy(value: string) {
this.platformUtilsService.copyToClipboard(value);
}
launchUri(url: string) {
this.platformUtilsService.launchUri(url);
}
}

View File

@@ -1,33 +0,0 @@
<div class="form-group">
<label [attr.for]="controlId">
{{ label }}
<small *ngIf="isRequired" class="text-muted form-text d-inline"
>({{ "required" | i18n }})</small
>
</label>
<input
[formControl]="internalControl"
class="form-control"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[attr.aria-invalid]="controlDir.control.invalid"
(blur)="onBlurInternal()"
/>
<div *ngIf="showDescribedBy" [attr.id]="describedById">
<small
*ngIf="helperText != null && !controlDir.control.hasError(helperTextSameAsError)"
class="form-text text-muted"
>
{{ helperText }}
</small>
<small class="error-inline" *ngIf="controlDir.control.hasError('required')" role="alert">
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{
controlDir.control.hasError(helperTextSameAsError)
? helperText
: ("fieldRequiredError" | i18n: label)
}}
</small>
</div>
</div>

View File

@@ -1,48 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-text[label][controlId]",
templateUrl: "input-text.component.html",
})
export class InputTextComponent extends BaseCvaComponent implements OnInit {
@Input() helperTextSameAsError: string;
@Input() requiredErrorMessage: string;
@Input() stripSpaces = false;
transformValue: (value: string) => string = null;
ngOnInit() {
super.ngOnInit();
if (this.stripSpaces) {
this.transformValue = this.doStripSpaces;
}
}
writeValue(value: string) {
this.internalControl.setValue(value == null ? "" : value);
}
protected onValueChangesInternal: any = (value: string) => {
let newValue = value;
if (this.transformValue != null) {
newValue = this.transformValue(value);
this.internalControl.setValue(newValue, { emitEvent: false });
}
this.onChange(newValue);
};
protected onValueChangeInternal(value: string) {
let newValue = value;
if (this.transformValue != null) {
newValue = this.transformValue(value);
this.internalControl.setValue(newValue, { emitEvent: false });
}
}
private doStripSpaces(value: string) {
return value.replace(/ /g, "");
}
}

View File

@@ -1,19 +0,0 @@
<div class="form-group">
<label [attr.for]="controlId">
{{ label }}
<small *ngIf="isRequired" class="text-muted form-text d-inline"
>({{ "required" | i18n }})</small
>
</label>
<select
class="form-control"
[attr.id]="controlId"
[attr.aria-invalid]="controlDir.control.invalid"
[formControl]="internalControl"
(blur)="onBlurInternal()"
>
<option *ngFor="let o of selectOptions" [ngValue]="o.value" disabled="{{ o.disabled }}">
{{ o.name }}
</option>
</select>
</div>

View File

@@ -1,14 +0,0 @@
import { Component, Input } from "@angular/core";
import { SelectOptions } from "jslib-angular/interfaces/selectOptions";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-select",
templateUrl: "select.component.html",
})
export class SelectComponent extends BaseCvaComponent {
@Input() selectOptions: SelectOptions[];
}

View File

@@ -1,448 +0,0 @@
<div class="page-header d-flex">
<h1>{{ "singleSignOn" | i18n }}</h1>
</div>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<form
#form
(ngSubmit)="submit()"
[formGroup]="ssoConfigForm"
[appApiAction]="formPromise"
*ngIf="!loading"
>
<p>
{{ "ssoPolicyHelpStart" | i18n }}
<a routerLink="../policies">{{ "ssoPolicyHelpLink" | i18n }}</a>
{{ "ssoPolicyHelpEnd" | i18n }}
<br />
{{ "ssoPolicyHelpKeyConnector" | i18n }}
</p>
<!-- Root form -->
<ng-container>
<app-input-checkbox
controlId="enabled"
[formControl]="enabled"
[label]="'allowSso' | i18n"
[helperText]="'allowSsoDesc' | i18n"
></app-input-checkbox>
<div class="form-group">
<label>{{ "memberDecryptionOption" | i18n }}</label>
<div class="form-check form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionPass"
[value]="false"
formControlName="keyConnectorEnabled"
/>
<label class="form-check-label" for="memberDecryptionPass">
{{ "masterPass" | i18n }}
<small>{{ "memberDecryptionPassDesc" | i18n }}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionKey"
[value]="true"
formControlName="keyConnectorEnabled"
[attr.disabled]="!organization.useKeyConnector || null"
/>
<label class="form-check-label" for="memberDecryptionKey">
{{ "keyConnector" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/about-key-connector/"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
<small>{{ "memberDecryptionKeyConnectorDesc" | i18n }}</small>
</label>
</div>
</div>
<!-- Key Connector -->
<ng-container *ngIf="ssoConfigForm.get('keyConnectorEnabled').value">
<app-callout type="warning" [useAlertRole]="true">
{{ "keyConnectorWarning" | i18n }}
</app-callout>
<div class="form-group">
<label for="keyConnectorUrl">
{{ "keyConnectorUrl" | i18n }}
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
</label>
<div class="input-group">
<input
class="form-control"
formControlName="keyConnectorUrl"
id="keyConnectorUrl"
aria-describedby="keyConnectorUrlDesc"
(change)="haveTestedKeyConnector = false"
appInputStripSpaces
appA11yInvalid
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
(click)="validateKeyConnectorUrl()"
[disabled]="!enableTestKeyConnector"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="keyConnectorUrl.pending"
></i>
<span *ngIf="!keyConnectorUrl.pending">
{{ "keyConnectorTest" | i18n }}
</span>
</button>
</div>
</div>
<div *ngIf="haveTestedKeyConnector" id="keyConnectorUrlDesc" aria-live="polite">
<small
class="error-inline"
*ngIf="keyConnectorUrl.hasError('invalidUrl'); else keyConnectorSuccess"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{ "keyConnectorTestFail" | i18n }}
</small>
<ng-template #keyConnectorSuccess>
<small class="text-success">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "keyConnectorTestSuccess" | i18n }}
</small>
</ng-template>
</div>
</div>
</ng-container>
<app-select
controlId="type"
[label]="'type' | i18n"
[selectOptions]="ssoTypeOptions"
formControlName="configType"
>
</app-select>
</ng-container>
<!-- OIDC -->
<div
*ngIf="ssoConfigForm.get('configType').value === ssoType.OpenIdConnect"
[formGroup]="openIdForm"
>
<div class="config-section">
<h2 class="secondary-header">{{ "openIdConnectConfig" | i18n }}</h2>
<app-input-text-readonly
[label]="'callbackPath' | i18n"
[controlValue]="callbackPath"
></app-input-text-readonly>
<app-input-text-readonly
[label]="'signedOutCallbackPath' | i18n"
[controlValue]="signedOutCallbackPath"
></app-input-text-readonly>
<app-input-text
[label]="'authority' | i18n"
controlId="authority"
[stripSpaces]="true"
formControlName="authority"
></app-input-text>
<app-input-text
[label]="'clientId' | i18n"
controlId="clientId"
[stripSpaces]="true"
formControlName="clientId"
></app-input-text>
<app-input-text
[label]="'clientSecret' | i18n"
controlId="clientSecret"
[stripSpaces]="true"
formControlName="clientSecret"
></app-input-text>
<app-input-text
[label]="'metadataAddress' | i18n"
controlId="metadataAddress"
[stripSpaces]="true"
[helperText]="'openIdAuthorityRequired' | i18n"
formControlName="metadataAddress"
></app-input-text>
<app-select
controlId="redirectBehavior"
[label]="'oidcRedirectBehavior' | i18n"
[selectOptions]="connectRedirectOptions"
formControlName="redirectBehavior"
>
</app-select>
<app-input-checkbox
controlId="getClaimsFromUserInfoEndpoint"
formControlName="getClaimsFromUserInfoEndpoint"
[label]="'getClaimsFromUserInfoEndpoint' | i18n"
></app-input-checkbox>
<!-- Optional customizations -->
<div
class="section-header d-flex flex-row align-items-center mt-3 mb-3"
(click)="toggleOpenIdCustomizations()"
>
<h3 class="mb-0 mr-2" id="customizations-header">
{{ "openIdOptionalCustomizations" | i18n }}
</h3>
<button
class="mb-1 btn btn-link"
type="button"
appStopClick
role="button"
aria-controls="customizations"
[attr.aria-expanded]="showOpenIdCustomizations"
aria-labelledby="customizations-header"
>
<i
class="bwi"
aria-hidden="true"
[ngClass]="{
'bwi-angle-down': !showOpenIdCustomizations,
'bwi-chevron-up': showOpenIdCustomizations
}"
></i>
</button>
</div>
<div id="customizations" [hidden]="!showOpenIdCustomizations">
<app-input-text
[label]="'additionalScopes' | i18n"
controlId="additionalScopes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalScopes"
></app-input-text>
<app-input-text
[label]="'additionalUserIdClaimTypes' | i18n"
controlId="additionalUserIdClaimTypes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalUserIdClaimTypes"
></app-input-text>
<app-input-text
[label]="'additionalEmailClaimTypes' | i18n"
controlId="additionalEmailClaimTypes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalEmailClaimTypes"
></app-input-text>
<app-input-text
[label]="'additionalNameClaimTypes' | i18n"
controlId="additionalNameClaimTypes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalNameClaimTypes"
></app-input-text>
<app-input-text
[label]="'acrValues' | i18n"
controlId="acrValues"
helperText="acr_values"
formControlName="acrValues"
></app-input-text>
<app-input-text
[label]="'expectedReturnAcrValue' | i18n"
controlId="expectedReturnAcrValue"
helperText="acr_validation"
formControlName="expectedReturnAcrValue"
></app-input-text>
</div>
</div>
</div>
<!-- SAML2 SP -->
<div *ngIf="ssoConfigForm.get('configType').value === ssoType.Saml2" [formGroup]="samlForm">
<!-- SAML2 SP -->
<div class="config-section">
<h2 class="secondary-header">{{ "samlSpConfig" | i18n }}</h2>
<app-input-text-readonly
[label]="'spEntityId' | i18n"
[controlValue]="spEntityId"
></app-input-text-readonly>
<app-input-text-readonly
[label]="'spMetadataUrl' | i18n"
[controlValue]="spMetadataUrl"
[showLaunch]="true"
></app-input-text-readonly>
<app-input-text-readonly
[label]="'spAcsUrl' | i18n"
[controlValue]="spAcsUrl"
></app-input-text-readonly>
<app-select
controlId="spNameIdFormat"
[label]="'spNameIdFormat' | i18n"
[selectOptions]="saml2NameIdFormatOptions"
formControlName="spNameIdFormat"
>
</app-select>
<app-select
controlId="spOutboundSigningAlgorithm"
[label]="'spOutboundSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="spOutboundSigningAlgorithm"
>
</app-select>
<app-select
controlId="spSigningBehavior"
[label]="'spSigningBehavior' | i18n"
[selectOptions]="saml2SigningBehaviourOptions"
formControlName="spSigningBehavior"
>
</app-select>
<app-select
controlId="spMinIncomingSigningAlgorithm"
[label]="'spMinIncomingSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="spMinIncomingSigningAlgorithm"
>
</app-select>
<app-input-checkbox
controlId="spWantAssertionsSigned"
formControlName="spWantAssertionsSigned"
[label]="'spWantAssertionsSigned' | i18n"
></app-input-checkbox>
<app-input-checkbox
controlId="spValidateCertificates"
formControlName="spValidateCertificates"
[label]="'spValidateCertificates' | i18n"
></app-input-checkbox>
</div>
<!-- SAML2 IDP -->
<div class="config-section">
<h2 class="secondary-header">{{ "samlIdpConfig" | i18n }}</h2>
<app-input-text
[label]="'idpEntityId' | i18n"
controlId="idpEntityId"
formControlName="idpEntityId"
></app-input-text>
<app-select
controlId="idpBindingType"
[label]="'idpBindingType' | i18n"
[selectOptions]="saml2BindingTypeOptions"
formControlName="idpBindingType"
>
</app-select>
<app-input-text
[label]="'idpSingleSignOnServiceUrl' | i18n"
controlId="idpSingleSignOnServiceUrl"
[helperText]="'idpSingleSignOnServiceUrlRequired' | i18n"
[stripSpaces]="true"
formControlName="idpSingleSignOnServiceUrl"
></app-input-text>
<app-input-text
[label]="'idpSingleLogoutServiceUrl' | i18n"
controlId="idpSingleLogoutServiceUrl"
[stripSpaces]="true"
formControlName="idpSingleLogoutServiceUrl"
></app-input-text>
<div class="form-group">
<label for="idpX509PublicCert">
{{ "idpX509PublicCert" | i18n }}
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
</label>
<textarea
formControlName="idpX509PublicCert"
class="form-control form-control-sm text-monospace"
rows="6"
id="idpX509PublicCert"
appA11yInvalid
aria-describedby="idpX509PublicCertDesc"
></textarea>
<small
id="idpX509PublicCertDesc"
class="error-inline"
role="alert"
*ngIf="samlForm.get('idpX509PublicCert').hasError('required')"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{ "fieldRequiredError" | i18n: ("idpX509PublicCert" | i18n) }}
</small>
</div>
<app-select
controlId="idpOutboundSigningAlgorithm"
[label]="'idpOutboundSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="idpOutboundSigningAlgorithm"
>
</app-select>
<!--TODO: Uncomment once Unsolicited IdP Response is supported-->
<!-- <app-input-checkbox
controlId="idpAllowUnsolicitedAuthnResponse"
formControlName="idpAllowUnsolicitedAuthnResponse"
[label]="'idpAllowUnsolicitedAuthnResponse' | i18n"
></app-input-checkbox> -->
<app-input-checkbox
controlId="idpAllowOutboundLogoutRequests"
formControlName="idpAllowOutboundLogoutRequests"
[label]="'idpAllowOutboundLogoutRequests' | i18n"
></app-input-checkbox>
<app-input-checkbox
controlId="idpWantAuthnRequestsSigned"
formControlName="idpWantAuthnRequestsSigned"
[label]="'idpSignAuthenticationRequests' | i18n"
></app-input-checkbox>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<div
id="errorSummary"
class="error-summary text-danger"
*ngIf="this.getErrorCount(ssoConfigForm) as errorCount"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{
(errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural") | i18n: errorCount
}}
</div>
</form>

View File

@@ -1,318 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { AbstractControl, FormBuilder, FormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { SelectOptions } from "jslib-angular/interfaces/selectOptions";
import { dirtyRequired } from "jslib-angular/validators/dirty.validator";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import {
OpenIdConnectRedirectBehavior,
Saml2BindingType,
Saml2NameIdFormat,
Saml2SigningBehavior,
SsoType,
} from "jslib-common/enums/ssoEnums";
import { Utils } from "jslib-common/misc/utils";
import { SsoConfigApi } from "jslib-common/models/api/ssoConfigApi";
import { Organization } from "jslib-common/models/domain/organization";
import { OrganizationSsoRequest } from "jslib-common/models/request/organization/organizationSsoRequest";
import { OrganizationSsoResponse } from "jslib-common/models/response/organization/organizationSsoResponse";
import { SsoConfigView } from "jslib-common/models/view/ssoConfigView";
const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
@Component({
selector: "app-org-manage-sso",
templateUrl: "sso.component.html",
})
export class SsoComponent implements OnInit {
readonly ssoType = SsoType;
readonly ssoTypeOptions: SelectOptions[] = [
{ name: this.i18nService.t("selectType"), value: SsoType.None, disabled: true },
{ name: "OpenID Connect", value: SsoType.OpenIdConnect },
{ name: "SAML 2.0", value: SsoType.Saml2 },
];
readonly samlSigningAlgorithms = [
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"http://www.w3.org/2000/09/xmldsig#rsa-sha384",
"http://www.w3.org/2000/09/xmldsig#rsa-sha512",
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
];
readonly saml2SigningBehaviourOptions: SelectOptions[] = [
{
name: "If IdP Wants Authn Requests Signed",
value: Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned,
},
{ name: "Always", value: Saml2SigningBehavior.Always },
{ name: "Never", value: Saml2SigningBehavior.Never },
];
readonly saml2BindingTypeOptions: SelectOptions[] = [
{ name: "Redirect", value: Saml2BindingType.HttpRedirect },
{ name: "HTTP POST", value: Saml2BindingType.HttpPost },
];
readonly saml2NameIdFormatOptions: SelectOptions[] = [
{ name: "Not Configured", value: Saml2NameIdFormat.NotConfigured },
{ name: "Unspecified", value: Saml2NameIdFormat.Unspecified },
{ name: "Email Address", value: Saml2NameIdFormat.EmailAddress },
{ name: "X.509 Subject Name", value: Saml2NameIdFormat.X509SubjectName },
{ name: "Windows Domain Qualified Name", value: Saml2NameIdFormat.WindowsDomainQualifiedName },
{ name: "Kerberos Principal Name", value: Saml2NameIdFormat.KerberosPrincipalName },
{ name: "Entity Identifier", value: Saml2NameIdFormat.EntityIdentifier },
{ name: "Persistent", value: Saml2NameIdFormat.Persistent },
{ name: "Transient", value: Saml2NameIdFormat.Transient },
];
readonly connectRedirectOptions: SelectOptions[] = [
{ name: "Redirect GET", value: OpenIdConnectRedirectBehavior.RedirectGet },
{ name: "Form POST", value: OpenIdConnectRedirectBehavior.FormPost },
];
showOpenIdCustomizations = false;
loading = true;
haveTestedKeyConnector = false;
organizationId: string;
organization: Organization;
formPromise: Promise<any>;
callbackPath: string;
signedOutCallbackPath: string;
spEntityId: string;
spMetadataUrl: string;
spAcsUrl: string;
enabled = this.formBuilder.control(false);
openIdForm = this.formBuilder.group(
{
authority: ["", dirtyRequired],
clientId: ["", dirtyRequired],
clientSecret: ["", dirtyRequired],
metadataAddress: [],
redirectBehavior: [OpenIdConnectRedirectBehavior.RedirectGet, dirtyRequired],
getClaimsFromUserInfoEndpoint: [],
additionalScopes: [],
additionalUserIdClaimTypes: [],
additionalEmailClaimTypes: [],
additionalNameClaimTypes: [],
acrValues: [],
expectedReturnAcrValue: [],
},
{
updateOn: "blur",
}
);
samlForm = this.formBuilder.group(
{
spNameIdFormat: [Saml2NameIdFormat.NotConfigured],
spOutboundSigningAlgorithm: [defaultSigningAlgorithm],
spSigningBehavior: [Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned],
spMinIncomingSigningAlgorithm: [defaultSigningAlgorithm],
spWantAssertionsSigned: [],
spValidateCertificates: [],
idpEntityId: ["", dirtyRequired],
idpBindingType: [Saml2BindingType.HttpRedirect],
idpSingleSignOnServiceUrl: [],
idpSingleLogoutServiceUrl: [],
idpX509PublicCert: ["", dirtyRequired],
idpOutboundSigningAlgorithm: [defaultSigningAlgorithm],
idpAllowUnsolicitedAuthnResponse: [],
idpAllowOutboundLogoutRequests: [true],
idpWantAuthnRequestsSigned: [],
},
{
updateOn: "blur",
}
);
ssoConfigForm = this.formBuilder.group({
configType: [SsoType.None],
keyConnectorEnabled: [false],
keyConnectorUrl: [""],
openId: this.openIdForm,
saml: this.samlForm,
});
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private organizationService: OrganizationService
) {}
async ngOnInit() {
this.ssoConfigForm.get("configType").valueChanges.subscribe((newType: SsoType) => {
if (newType === SsoType.OpenIdConnect) {
this.openIdForm.enable();
this.samlForm.disable();
} else if (newType === SsoType.Saml2) {
this.openIdForm.disable();
this.samlForm.enable();
} else {
this.openIdForm.disable();
this.samlForm.disable();
}
});
this.samlForm
.get("spSigningBehavior")
.valueChanges.subscribe(() =>
this.samlForm.get("idpX509PublicCert").updateValueAndValidity()
);
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
});
}
async load() {
this.organization = await this.organizationService.get(this.organizationId);
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
this.populateForm(ssoSettings);
this.callbackPath = ssoSettings.urls.callbackPath;
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
this.spEntityId = ssoSettings.urls.spEntityId;
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
this.loading = false;
}
async submit() {
this.validateForm(this.ssoConfigForm);
if (this.ssoConfigForm.get("keyConnectorEnabled").value) {
await this.validateKeyConnectorUrl();
}
if (!this.ssoConfigForm.valid) {
this.readOutErrors();
return;
}
const request = new OrganizationSsoRequest();
request.enabled = this.enabled.value;
request.data = SsoConfigApi.fromView(this.ssoConfigForm.value as SsoConfigView);
this.formPromise = this.apiService.postOrganizationSso(this.organizationId, request);
try {
const response = await this.formPromise;
this.populateForm(response);
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
} catch {
// Logged by appApiAction, do nothing
}
this.formPromise = null;
}
async validateKeyConnectorUrl() {
if (this.haveTestedKeyConnector) {
return;
}
this.keyConnectorUrl.markAsPending();
try {
await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
this.keyConnectorUrl.updateValueAndValidity();
} catch {
this.keyConnectorUrl.setErrors({
invalidUrl: true,
});
}
this.haveTestedKeyConnector = true;
}
toggleOpenIdCustomizations() {
this.showOpenIdCustomizations = !this.showOpenIdCustomizations;
}
getErrorCount(form: FormGroup): number {
return Object.values(form.controls).reduce((acc: number, control: AbstractControl) => {
if (control instanceof FormGroup) {
return acc + this.getErrorCount(control);
}
if (control.errors == null) {
return acc;
}
return acc + Object.keys(control.errors).length;
}, 0);
}
get enableTestKeyConnector() {
return (
this.ssoConfigForm.get("keyConnectorEnabled").value &&
!Utils.isNullOrWhitespace(this.keyConnectorUrl?.value)
);
}
get keyConnectorUrl() {
return this.ssoConfigForm.get("keyConnectorUrl");
}
get samlSigningAlgorithmOptions(): SelectOptions[] {
return this.samlSigningAlgorithms.map((algorithm) => ({ name: algorithm, value: algorithm }));
}
private validateForm(form: FormGroup) {
Object.values(form.controls).forEach((control: AbstractControl) => {
if (control.disabled) {
return;
}
if (control instanceof FormGroup) {
this.validateForm(control);
} else {
control.markAsDirty();
control.markAsTouched();
control.updateValueAndValidity();
}
});
}
private populateForm(ssoSettings: OrganizationSsoResponse) {
this.enabled.setValue(ssoSettings.enabled);
if (ssoSettings.data != null) {
const ssoConfigView = new SsoConfigView(ssoSettings.data);
this.ssoConfigForm.patchValue(ssoConfigView);
}
}
private readOutErrors() {
const errorText = this.i18nService.t("error");
const errorCount = this.getErrorCount(this.ssoConfigForm);
const errorCountText = this.i18nService.t(
errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural",
errorCount.toString()
);
const div = document.createElement("div");
div.className = "sr-only";
div.id = "srErrorCount";
div.setAttribute("aria-live", "polite");
div.innerText = errorText + ": " + errorCountText;
const existing = document.getElementById("srErrorCount");
if (existing != null) {
existing.remove();
}
document.body.append(div);
}
}

View File

@@ -1,44 +0,0 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "jslib-angular/guards/auth.guard";
import { Permissions } from "jslib-common/enums/permissions";
import { PermissionsGuard } from "src/app/organizations/guards/permissions.guard";
import { OrganizationLayoutComponent } from "src/app/organizations/layouts/organization-layout.component";
import { ManageComponent } from "src/app/organizations/manage/manage.component";
import { NavigationPermissionsService } from "src/app/organizations/services/navigation-permissions.service";
import { SsoComponent } from "./manage/sso.component";
const routes: Routes = [
{
path: "organizations/:organizationId",
component: OrganizationLayoutComponent,
canActivate: [AuthGuard, PermissionsGuard],
children: [
{
path: "manage",
component: ManageComponent,
canActivate: [PermissionsGuard],
data: {
permissions: NavigationPermissionsService.getPermissions("manage").concat(
Permissions.ManageSso
),
},
children: [
{
path: "sso",
component: SsoComponent,
},
],
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrganizationsRoutingModule {}

View File

@@ -1,32 +0,0 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { JslibModule } from "jslib-angular/jslib.module";
import { InputCheckboxComponent } from "./components/input-checkbox.component";
import { InputTextReadOnlyComponent } from "./components/input-text-readonly.component";
import { InputTextComponent } from "./components/input-text.component";
import { SelectComponent } from "./components/select.component";
import { SsoComponent } from "./manage/sso.component";
import { OrganizationsRoutingModule } from "./organizations-routing.module";
// Form components are for use in the SSO Configuration Form only and should not be exported for use elsewhere.
// They will be deprecated by the Component Library.
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
JslibModule,
OrganizationsRoutingModule,
],
declarations: [
InputCheckboxComponent,
InputTextComponent,
InputTextReadOnlyComponent,
SelectComponent,
SsoComponent,
],
})
export class OrganizationsModule {}

View File

@@ -7,7 +7,6 @@
"buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr"
},
"dev": {
"port": 8080,
"allowedHosts": "auto"
}
}

View File

@@ -1,9 +1 @@
{
"dev": {
"proxyApi": "http://localhost:4001",
"proxyIdentity": "http://localhost:33657",
"proxyEvents": "http://localhost:46274",
"proxyNotifications": "http://localhost:61841",
"port": 8081
}
}
{}

102
extra-webpack.config.ts Normal file
View File

@@ -0,0 +1,102 @@
import { CustomWebpackBrowserSchema, TargetOptions } from "@angular-builders/custom-webpack";
import * as webpack from "webpack";
import * as CopyPlugin from "copy-webpack-plugin";
import * as HtmlWebpackPlugin from "html-webpack-plugin";
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// We need to extend the default angular webpack to support web vault specific logic.
//
// Namely:
// * Connectors
// * Environment variables
export default (
config: webpack.Configuration,
options: CustomWebpackBrowserSchema,
targetOptions: TargetOptions
) => {
const plugins = [
new HtmlWebpackPlugin({
template: "./projects/web-vault-internal/src/connectors/duo.html",
filename: "duo-connector.html",
chunks: ["connectors/duo"],
}),
new HtmlWebpackPlugin({
template: "./projects/web-vault-internal/src/connectors/webauthn.html",
filename: "webauthn-connector.html",
chunks: ["connectors/webauthn"],
}),
new HtmlWebpackPlugin({
template: "./projects/web-vault-internal/src/connectors/webauthn-mobile.html",
filename: "webauthn-mobile-connector.html",
chunks: ["connectors/webauthn"],
}),
new HtmlWebpackPlugin({
template: "./projects/web-vault-internal/src/connectors/webauthn-fallback.html",
filename: "webauthn-fallback-connector.html",
chunks: ["connectors/webauthn-fallback"],
}),
new HtmlWebpackPlugin({
template: "./projects/web-vault-internal/src/connectors/sso.html",
filename: "sso-connector.html",
chunks: ["connectors/sso"],
}),
new HtmlWebpackPlugin({
template: "./projects/web-vault-internal/src/connectors/captcha.html",
filename: "captcha-connector.html",
chunks: ["connectors/captcha"],
}),
new HtmlWebpackPlugin({
template: "./projects/web-vault-internal/src/connectors/captcha-mobile.html",
filename: "captcha-mobile-connector.html",
chunks: ["connectors/captcha"],
}),
// TODO: Replace with angular cli copy
new CopyPlugin({
patterns: [
{
from: "./projects/web-vault-internal/src/version.json",
transform(content, path) {
return content.toString().replace("process.env.APPLICATION_VERSION", "12");
},
},
],
}),
new webpack.EnvironmentPlugin({
ENV: "development",
NODE_ENV: "development",
APPLICATION_VERSION: "123",
CACHE_TAG: Math.random().toString(36).substring(7),
URLS: {},
STRIPE_KEY: "",
BRAINTREE_KEY: "",
PAYPAL_CONFIG: {},
}),
new webpack.ProvidePlugin({
process: "process/browser",
}),
];
config.plugins?.push(...plugins);
(config.resolve as any).fallback = {
buffer: false,
util: require.resolve("util/"),
assert: false,
};
// TODO: Figure out if we have to cast it to any
(config.entry as any)["connectors/webauthn"] = [
"./projects/web-vault-internal/src/connectors/webauthn.ts",
];
(config.entry as any)["connectors/webauthn-fallback"] = [
"./projects/web-vault-internal/src/connectors/webauthn-fallback.ts",
];
(config.entry as any)["connectors/duo"] = ["./projects/web-vault-internal/src/connectors/duo.ts"];
(config.entry as any)["connectors/sso"] = ["./projects/web-vault-internal/src/connectors/sso.ts"];
(config.entry as any)["connectors/captcha"] = [
"./projects/web-vault-internal/src/connectors/captcha.ts",
];
return config;
};

2
jslib

Submodule jslib updated: 3cb94623e2...e372bf242b

41
karma.conf.js Normal file
View File

@@ -0,0 +1,41 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: "",
frameworks: ["jasmine", "@angular-devkit/build-angular"],
plugins: [
require("karma-jasmine"),
require("karma-chrome-launcher"),
require("karma-jasmine-html-reporter"),
require("karma-coverage"),
require("@angular-devkit/build-angular/plugins/karma"),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter: {
dir: require("path").join(__dirname, "./coverage/lawl"),
subdir: ".",
reporters: [{ type: "html" }, { type: "text-summary" }],
},
reporters: ["progress", "kjhtml"],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ["Chrome"],
singleRun: false,
restartOnFileChange: true,
});
};

29913
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,119 +1,83 @@
{
"name": "@bitwarden/web-vault",
"version": "2.28.1",
"version": "2.25.1",
"license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web",
"scripts": {
"sub:init": "git submodule update --init --recursive",
"sub:update": "git submodule update --remote",
"sub:pull": "git submodule foreach git pull origin master",
"preinstall": "npm run sub:init",
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
"symlink:mac": "npm run symlink:lin",
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"build:oss": "webpack",
"build:bit": "webpack -c bitwarden_license/webpack.config.js",
"build:oss:watch": "webpack serve",
"build:bit:watch": "webpack serve -c bitwarden_license/webpack.config.js",
"build:bit:dev": "cross-env ENV=development npm run build:bit",
"build:bit:dev:watch": "cross-env ENV=development npm run build:bit:watch",
"build:bit:qa": "cross-env NODE_ENV=production ENV=qa npm run build:bit",
"build:bit:cloud": "cross-env NODE_ENV=production ENV=cloud npm run build:bit",
"build:oss:selfhost:watch": "cross-env ENV=selfhosted npm run build:oss:watch",
"build:bit:selfhost:watch": "cross-env ENV=selfhosted npm run build:bit:watch",
"build:oss:selfhost:prod": "cross-env ENV=selfhosted NODE_ENV=production npm run build:oss",
"build:bit:selfhost:prod": "cross-env ENV=selfhosted NODE_ENV=production npm run build:bit",
"clean:l10n": "git push origin --delete l10n_master",
"dist:bit:cloud": "npm run build:bit:cloud",
"dist:oss:selfhost": "npm run build:oss:selfhost:prod",
"dist:bit:selfhost": "npm run build:bit:selfhost:prod",
"deploy": "npm run dist:bit && gh-pages -d build",
"deploy:dev": "npm run dist:bit && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint . --fix",
"prettier": "prettier --write .",
"prepare": "husky install"
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"devDependencies": {
"@angular/compiler-cli": "^12.2.13",
"@ngtools/webpack": "^12.2.13",
"@types/jquery": "^3.5.5",
"@types/node": "^16.11.12",
"@types/webcrypto": "^0.0.28",
"@types/webpack": "^5.28.0",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"autoprefixer": "^10.4.2",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"eslint": "^8.7.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.4",
"gh-pages": "^3.1.0",
"html-loader": "^3.0.1",
"html-webpack-injector": "1.1.4",
"html-webpack-plugin": "^5.5.0",
"husky": "^7.0.4",
"lint-staged": "^12.1.2",
"mini-css-extract-plugin": "^2.4.5",
"postcss": "^8.4.6",
"postcss-loader": "^6.2.1",
"prettier": "2.5.1",
"process": "^0.11.10",
"rimraf": "^3.0.2",
"sass": "^1.32.10",
"sass-loader": "^12.4.0",
"style-loader": "^3.3.1",
"tailwindcss": "^3.0.18",
"terser-webpack-plugin": "^5.2.5",
"ts-loader": "^9.2.5",
"typescript": "4.3.5",
"util": "^0.12.4",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.6.0"
"private": true,
"lint-staged": {
"*": "prettier --ignore-unknown --write",
"*.png": "node scripts/optimize.js"
},
"dependencies": {
"@angular/animations": "^12.2.13",
"@angular/cdk": "^12.2.13",
"@angular/common": "^12.2.13",
"@angular/compiler": "^12.2.13",
"@angular/core": "^12.2.13",
"@angular/forms": "^12.2.13",
"@angular/platform-browser": "^12.2.13",
"@angular/platform-browser-dynamic": "^12.2.13",
"@angular/router": "^12.2.13",
"@angular/animations": "~12.2.0",
"@angular/cdk": "~12.2.0",
"@angular/common": "~12.2.0",
"@angular/compiler": "~12.2.0",
"@angular/core": "~12.2.0",
"@angular/forms": "~12.2.0",
"@angular/platform-browser": "~12.2.0",
"@angular/platform-browser-dynamic": "~12.2.0",
"@angular/router": "~12.2.0",
"@bitwarden/jslib-angular": "file:jslib/angular",
"@bitwarden/jslib-common": "file:jslib/common",
"@microsoft/signalr": "5.0.10",
"@microsoft/signalr-protocol-msgpack": "5.0.10",
"assert": "^2.0.0",
"big-integer": "1.6.48",
"bootstrap": "4.6.0",
"braintree-web-drop-in": "1.33.1",
"braintree-web-drop-in": "1.30.1",
"browser-hrtime": "^1.1.8",
"core-js": "^3.11.0",
"buffer": "^6.0.3",
"date-input-polyfill": "^2.14.0",
"duo_web_sdk": "github:duosecurity/duo_web_sdk",
"font-awesome": "4.7.0",
"jquery": "3.6.0",
"jszip": "^3.7.1",
"lunr": "^2.3.9",
"ngx-infinite-scroll": "^10.0.1",
"ngx-toastr": "14.1.4",
"node-forge": "^1.3.1",
"popper.js": "1.16.1",
"node-forge": "^0.10.0",
"papaparse": "^5.3.1",
"process": "^0.11.10",
"qrious": "4.0.2",
"rxjs": "^7.4.0",
"sweetalert2": "^10.16.6",
"webcrypto-shim": "0.1.7",
"whatwg-fetch": "3.6.2",
"zone.js": "0.11.4"
"sweetalert2": "^10.16.9",
"tslib": "^2.3.0",
"util": "^0.12.4",
"zone.js": "~0.11.4",
"zxcvbn": "^4.4.2"
},
"engines": {
"node": "~16",
"npm": "~8"
},
"lint-staged": {
"./!(jslib)**": "prettier --ignore-unknown --write",
"*.ts": "eslint --fix",
"*.png": "node scripts/optimize.js"
"devDependencies": {
"@angular-builders/custom-webpack": "^12.1.3",
"@angular-devkit/build-angular": "~12.2.15",
"@angular/cli": "~12.2.15",
"@angular/compiler-cli": "~12.2.0",
"@types/duo_web_sdk": "^2.7.1",
"@types/jasmine": "~3.8.0",
"@types/jquery": "^3.5.13",
"@types/lunr": "^2.3.4",
"@types/node": "^16.11.21",
"@types/node-forge": "^0.9.7",
"@types/papaparse": "^5.3.1",
"@types/zxcvbn": "^4.4.1",
"copy-webpack-plugin": "^10.2.1",
"html-webpack-plugin": "^5.5.0",
"husky": "^7.0.4",
"jasmine-core": "~3.8.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"lint-staged": "^12.1.2",
"ng-packagr": "^12.1.1",
"prettier": "^2.5.1",
"typescript": "~4.3.5"
}
}

View File

@@ -1,4 +0,0 @@
/* eslint-disable no-undef */
module.exports = {
plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")],
};

View File

@@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@@ -0,0 +1,41 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: "",
frameworks: ["jasmine", "@angular-devkit/build-angular"],
plugins: [
require("karma-jasmine"),
require("karma-chrome-launcher"),
require("karma-jasmine-html-reporter"),
require("karma-coverage"),
require("@angular-devkit/build-angular/plugins/karma"),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter: {
dir: require("path").join(__dirname, "../../coverage/oss-vault"),
subdir: ".",
reporters: [{ type: "html" }, { type: "text-summary" }],
},
reporters: ["progress", "kjhtml"],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ["Chrome"],
singleRun: false,
restartOnFileChange: true,
});
};

View File

@@ -0,0 +1,10 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@@ -0,0 +1,839 @@
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * Delete the template below * * * * * * * * * * -->
<!-- * * * * * * * to get started with your project! * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<style>
:host {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px;
color: #333;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 8px 0;
}
p {
margin: 0;
}
.spacer {
flex: 1;
}
.toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 60px;
display: flex;
align-items: center;
background-color: #1976d2;
color: white;
font-weight: 600;
}
.toolbar img {
margin: 0 16px;
}
.toolbar #twitter-logo {
height: 40px;
margin: 0 8px;
}
.toolbar #youtube-logo {
height: 40px;
margin: 0 16px;
}
.toolbar #twitter-logo:hover,
.toolbar #youtube-logo:hover {
opacity: 0.8;
}
.content {
display: flex;
margin: 82px auto 32px;
padding: 0 16px;
max-width: 960px;
flex-direction: column;
align-items: center;
}
svg.material-icons {
height: 24px;
width: auto;
}
svg.material-icons:not(:last-child) {
margin-right: 8px;
}
.card svg.material-icons path {
fill: #888;
}
.card-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 16px;
}
.card {
all: unset;
border-radius: 4px;
border: 1px solid #eee;
background-color: #fafafa;
height: 40px;
width: 200px;
margin: 0 8px 16px;
padding: 16px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
line-height: 24px;
}
.card-container .card:not(:last-child) {
margin-right: 0;
}
.card.card-small {
height: 16px;
width: 168px;
}
.card-container .card:not(.highlight-card) {
cursor: pointer;
}
.card-container .card:not(.highlight-card):hover {
transform: translateY(-3px);
box-shadow: 0 4px 17px rgba(0, 0, 0, 0.35);
}
.card-container .card:not(.highlight-card):hover .material-icons path {
fill: rgb(105, 103, 103);
}
.card.highlight-card {
background-color: #1976d2;
color: white;
font-weight: 600;
border: none;
width: auto;
min-width: 30%;
position: relative;
}
.card.card.highlight-card span {
margin-left: 60px;
}
svg#rocket {
width: 80px;
position: absolute;
left: -10px;
top: -24px;
}
svg#rocket-smoke {
height: calc(100vh - 95px);
position: absolute;
top: 10px;
right: 180px;
z-index: -10;
}
a,
a:visited,
a:hover {
color: #1976d2;
text-decoration: none;
}
a:hover {
color: #125699;
}
.terminal {
position: relative;
width: 80%;
max-width: 600px;
border-radius: 6px;
padding-top: 45px;
margin-top: 8px;
overflow: hidden;
background-color: rgb(15, 15, 16);
}
.terminal::before {
content: "\2022 \2022 \2022";
position: absolute;
top: 0;
left: 0;
height: 4px;
background: rgb(58, 58, 58);
color: #c2c3c4;
width: 100%;
font-size: 2rem;
line-height: 0;
padding: 14px 0;
text-indent: 4px;
}
.terminal pre {
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
color: white;
padding: 0 1rem 1rem;
margin: 0;
}
.circle-link {
height: 40px;
width: 40px;
border-radius: 40px;
margin: 8px;
background-color: white;
border: 1px solid #eeeeee;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
transition: 1s ease-out;
}
.circle-link:hover {
transform: translateY(-0.25rem);
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2);
}
footer {
margin-top: 8px;
display: flex;
align-items: center;
line-height: 20px;
}
footer a {
display: flex;
align-items: center;
}
.github-star-badge {
color: #24292e;
display: flex;
align-items: center;
font-size: 12px;
padding: 3px 10px;
border: 1px solid rgba(27, 31, 35, 0.2);
border-radius: 3px;
background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
margin-left: 4px;
font-weight: 600;
}
.github-star-badge:hover {
background-image: linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%);
border-color: rgba(27, 31, 35, 0.35);
background-position: -0.5em;
}
.github-star-badge .material-icons {
height: 16px;
width: 16px;
margin-right: 4px;
}
svg#clouds {
position: fixed;
bottom: -160px;
left: -230px;
z-index: -10;
width: 1920px;
}
/* Responsive Styles */
@media screen and (max-width: 767px) {
.card-container > *:not(.circle-link),
.terminal {
width: 100%;
}
.card:not(.highlight-card) {
height: 16px;
margin: 8px 0;
}
.card.highlight-card span {
margin-left: 72px;
}
svg#rocket-smoke {
right: 120px;
transform: rotate(-5deg);
}
}
@media screen and (max-width: 575px) {
svg#rocket-smoke {
display: none;
visibility: hidden;
}
}
</style>
<!-- Toolbar -->
<div class="toolbar" role="banner">
<img
width="40"
alt="Angular Logo"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg=="
/>
<span>Welcome</span>
<div class="spacer"></div>
<a
aria-label="Angular on twitter"
target="_blank"
rel="noopener"
href="https://twitter.com/angular"
title="Twitter"
>
<svg
id="twitter-logo"
height="24"
data-name="Logo"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400"
>
<rect width="400" height="400" fill="none" />
<path
d="M153.62,301.59c94.34,0,145.94-78.16,145.94-145.94,0-2.22,0-4.43-.15-6.63A104.36,104.36,0,0,0,325,122.47a102.38,102.38,0,0,1-29.46,8.07,51.47,51.47,0,0,0,22.55-28.37,102.79,102.79,0,0,1-32.57,12.45,51.34,51.34,0,0,0-87.41,46.78A145.62,145.62,0,0,1,92.4,107.81a51.33,51.33,0,0,0,15.88,68.47A50.91,50.91,0,0,1,85,169.86c0,.21,0,.43,0,.65a51.31,51.31,0,0,0,41.15,50.28,51.21,51.21,0,0,1-23.16.88,51.35,51.35,0,0,0,47.92,35.62,102.92,102.92,0,0,1-63.7,22A104.41,104.41,0,0,1,75,278.55a145.21,145.21,0,0,0,78.62,23"
fill="#fff"
/>
</svg>
</a>
<a
aria-label="Angular on YouTube"
target="_blank"
rel="noopener"
href="https://youtube.com/angular"
title="YouTube"
>
<svg
id="youtube-logo"
height="24"
width="24"
data-name="Logo"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="#fff"
>
<path d="M0 0h24v24H0V0z" fill="none" />
<path
d="M21.58 7.19c-.23-.86-.91-1.54-1.77-1.77C18.25 5 12 5 12 5s-6.25 0-7.81.42c-.86.23-1.54.91-1.77 1.77C2 8.75 2 12 2 12s0 3.25.42 4.81c.23.86.91 1.54 1.77 1.77C5.75 19 12 19 12 19s6.25 0 7.81-.42c.86-.23 1.54-.91 1.77-1.77C22 15.25 22 12 22 12s0-3.25-.42-4.81zM10 15V9l5.2 3-5.2 3z"
/>
</svg>
</a>
</div>
<div class="content" role="main">
<!-- Highlight Card -->
<div class="card highlight-card card-small">
<svg
id="rocket"
xmlns="http://www.w3.org/2000/svg"
width="101.678"
height="101.678"
viewBox="0 0 101.678 101.678"
>
<title>Rocket Ship</title>
<g id="Group_83" data-name="Group 83" transform="translate(-141 -696)">
<circle
id="Ellipse_8"
data-name="Ellipse 8"
cx="50.839"
cy="50.839"
r="50.839"
transform="translate(141 696)"
fill="#dd0031"
/>
<g id="Group_47" data-name="Group 47" transform="translate(165.185 720.185)">
<path
id="Path_33"
data-name="Path 33"
d="M3.4,42.615a3.084,3.084,0,0,0,3.553,3.553,21.419,21.419,0,0,0,12.215-6.107L9.511,30.4A21.419,21.419,0,0,0,3.4,42.615Z"
transform="translate(0.371 3.363)"
fill="#fff"
/>
<path
id="Path_34"
data-name="Path 34"
d="M53.3,3.221A3.09,3.09,0,0,0,50.081,0,48.227,48.227,0,0,0,18.322,13.437c-6-1.666-14.991-1.221-18.322,7.218A33.892,33.892,0,0,1,9.439,25.1l-.333.666a3.013,3.013,0,0,0,.555,3.553L23.985,43.641a2.9,2.9,0,0,0,3.553.555l.666-.333A33.892,33.892,0,0,1,32.647,53.3c8.55-3.664,8.884-12.326,7.218-18.322A48.227,48.227,0,0,0,53.3,3.221ZM34.424,9.772a6.439,6.439,0,1,1,9.106,9.106,6.368,6.368,0,0,1-9.106,0A6.467,6.467,0,0,1,34.424,9.772Z"
transform="translate(0 0.005)"
fill="#fff"
/>
</g>
</g>
</svg>
<span>{{ title }} app is running!</span>
<svg
id="rocket-smoke"
xmlns="http://www.w3.org/2000/svg"
width="516.119"
height="1083.632"
viewBox="0 0 516.119 1083.632"
>
<title>Rocket Ship Smoke</title>
<path
id="Path_40"
data-name="Path 40"
d="M644.6,141S143.02,215.537,147.049,870.207s342.774,201.755,342.774,201.755S404.659,847.213,388.815,762.2c-27.116-145.51-11.551-384.124,271.9-609.1C671.15,139.365,644.6,141,644.6,141Z"
transform="translate(-147.025 -140.939)"
fill="#f5f5f5"
/>
</svg>
</div>
<!-- Resources -->
<h2>Resources</h2>
<p>Here are some links to help you get started:</p>
<div class="card-container">
<a class="card" target="_blank" rel="noopener" href="https://angular.io/tutorial">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M5 13.18v4L12 21l7-3.82v-4L12 17l-7-3.82zM12 3L1 9l11 6 9-4.91V17h2V9L12 3z" />
</svg>
<span>Learn Angular</span>
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
</svg>
</a>
<a class="card" target="_blank" rel="noopener" href="https://angular.io/cli">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"
/>
</svg>
<span>CLI Documentation</span>
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
</svg>
</a>
<a class="card" target="_blank" rel="noopener" href="https://blog.angular.io/">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
d="M13.5.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73l.03-.36C5.21 7.51 4 10.62 4 14c0 4.42 3.58 8 8 8s8-3.58 8-8C20 8.61 17.41 3.8 13.5.67zM11.71 19c-1.78 0-3.22-1.4-3.22-3.14 0-1.62 1.05-2.76 2.81-3.12 1.77-.36 3.6-1.21 4.62-2.58.39 1.29.59 2.65.59 4.04 0 2.65-2.15 4.8-4.8 4.8z"
/>
</svg>
<span>Angular Blog</span>
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
</svg>
</a>
<a class="card" target="_blank" rel="noopener" href="https://angular.io/devtools/">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
enable-background="new 0 0 24 24"
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
>
<g><rect fill="none" height="24" width="24" /></g>
<g>
<g>
<path
d="M14.73,13.31C15.52,12.24,16,10.93,16,9.5C16,5.91,13.09,3,9.5,3S3,5.91,3,9.5C3,13.09,5.91,16,9.5,16 c1.43,0,2.74-0.48,3.81-1.27L19.59,21L21,19.59L14.73,13.31z M9.5,14C7.01,14,5,11.99,5,9.5S7.01,5,9.5,5S14,7.01,14,9.5 S11.99,14,9.5,14z"
/>
<polygon
points="10.29,8.44 9.5,6 8.71,8.44 6.25,8.44 8.26,10.03 7.49,12.5 9.5,10.97 11.51,12.5 10.74,10.03 12.75,8.44"
/>
</g>
</g>
</svg>
<span>Angular DevTools</span>
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
</svg>
</a>
</div>
<!-- Next Steps -->
<h2>Next Steps</h2>
<p>What do you want to do next with your app?</p>
<input type="hidden" #selection />
<div class="card-container">
<button class="card card-small" (click)="selection.value = 'component'" tabindex="0">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
<span>New Component</span>
</button>
<button class="card card-small" (click)="selection.value = 'material'" tabindex="0">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
<span>Angular Material</span>
</button>
<button class="card card-small" (click)="selection.value = 'pwa'" tabindex="0">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
<span>Add PWA Support</span>
</button>
<button class="card card-small" (click)="selection.value = 'dependency'" tabindex="0">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
<span>Add Dependency</span>
</button>
<button class="card card-small" (click)="selection.value = 'test'" tabindex="0">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
<span>Run and Watch Tests</span>
</button>
<button class="card card-small" (click)="selection.value = 'build'" tabindex="0">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
<span>Build for Production</span>
</button>
</div>
<!-- Terminal -->
<div class="terminal" [ngSwitch]="selection.value">
<pre *ngSwitchDefault>ng generate component xyz</pre>
<pre *ngSwitchCase="'material'">ng add @angular/material</pre>
<pre *ngSwitchCase="'pwa'">ng add @angular/pwa</pre>
<pre *ngSwitchCase="'dependency'">ng add _____</pre>
<pre *ngSwitchCase="'test'">ng test</pre>
<pre *ngSwitchCase="'build'">ng build</pre>
</div>
<!-- Links -->
<div class="card-container">
<a
class="circle-link"
title="Animations"
href="https://angular.io/guide/animations"
target="_blank"
rel="noopener"
>
<svg
id="Group_20"
data-name="Group 20"
xmlns="http://www.w3.org/2000/svg"
width="21.813"
height="23.453"
viewBox="0 0 21.813 23.453"
>
<path
id="Path_15"
data-name="Path 15"
d="M4099.584,972.736h0l-10.882,3.9,1.637,14.4,9.245,5.153,9.245-5.153,1.686-14.4Z"
transform="translate(-4088.702 -972.736)"
fill="#ffa726"
/>
<path
id="Path_16"
data-name="Path 16"
d="M4181.516,972.736v23.453l9.245-5.153,1.686-14.4Z"
transform="translate(-4170.633 -972.736)"
fill="#fb8c00"
/>
<path
id="Path_17"
data-name="Path 17"
d="M4137.529,1076.127l-7.7-3.723,4.417-2.721,7.753,3.723Z"
transform="translate(-4125.003 -1058.315)"
fill="#ffe0b2"
/>
<path
id="Path_18"
data-name="Path 18"
d="M4137.529,1051.705l-7.7-3.723,4.417-2.721,7.753,3.723Z"
transform="translate(-4125.003 -1036.757)"
fill="#fff3e0"
/>
<path
id="Path_19"
data-name="Path 19"
d="M4137.529,1027.283l-7.7-3.723,4.417-2.721,7.753,3.723Z"
transform="translate(-4125.003 -1015.199)"
fill="#fff"
/>
</svg>
</a>
<a
class="circle-link"
title="CLI"
href="https://cli.angular.io/"
target="_blank"
rel="noopener"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="21.762"
height="23.447"
viewBox="0 0 21.762 23.447"
>
<title>Angular CLI Logo</title>
<g id="Group_21" data-name="Group 21" transform="translate(0)">
<path
id="Path_20"
data-name="Path 20"
d="M2660.313,313.618h0l-10.833,3.9,1.637,14.4,9.2,5.152,9.244-5.152,1.685-14.4Z"
transform="translate(-2649.48 -313.618)"
fill="#37474f"
/>
<path
id="Path_21"
data-name="Path 21"
d="M2741.883,313.618v23.447l9.244-5.152,1.685-14.4Z"
transform="translate(-2731.05 -313.618)"
fill="#263238"
/>
<path
id="Path_22"
data-name="Path 22"
d="M2692.293,379.169h11.724V368.618h-11.724Zm11.159-.6h-10.608v-9.345h10.621v9.345Z"
transform="translate(-2687.274 -362.17)"
fill="#fff"
/>
<path
id="Path_23"
data-name="Path 23"
d="M2709.331,393.688l.4.416,2.265-2.28-2.294-2.294-.4.4,1.893,1.893Z"
transform="translate(-2702.289 -380.631)"
fill="#fff"
/>
<rect
id="Rectangle_12"
data-name="Rectangle 12"
width="3.517"
height="0.469"
transform="translate(9.709 13.744)"
fill="#fff"
/>
</g>
</svg>
</a>
<a
class="circle-link"
title="Find a Local Meetup"
href="https://www.meetup.com/find/?keywords=angular"
target="_blank"
rel="noopener"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24.607"
height="23.447"
viewBox="0 0 24.607 23.447"
>
<title>Meetup Logo</title>
<path
id="logo--mSwarm"
d="M21.221,14.95A4.393,4.393,0,0,1,17.6,19.281a4.452,4.452,0,0,1-.8.069c-.09,0-.125.035-.154.117a2.939,2.939,0,0,1-2.506,2.091,2.868,2.868,0,0,1-2.248-.624.168.168,0,0,0-.245-.005,3.926,3.926,0,0,1-2.589.741,4.015,4.015,0,0,1-3.7-3.347,2.7,2.7,0,0,1-.043-.38c0-.106-.042-.146-.143-.166a3.524,3.524,0,0,1-1.516-.69A3.623,3.623,0,0,1,2.23,14.557a3.66,3.66,0,0,1,1.077-3.085.138.138,0,0,0,.026-.2,3.348,3.348,0,0,1-.451-1.821,3.46,3.46,0,0,1,2.749-3.28.44.44,0,0,0,.355-.281,5.072,5.072,0,0,1,3.863-3,5.028,5.028,0,0,1,3.555.666.31.31,0,0,0,.271.03A4.5,4.5,0,0,1,18.3,4.7a4.4,4.4,0,0,1,1.334,2.751,3.658,3.658,0,0,1,.022.706.131.131,0,0,0,.1.157,2.432,2.432,0,0,1,1.574,1.645,2.464,2.464,0,0,1-.7,2.616c-.065.064-.051.1-.014.166A4.321,4.321,0,0,1,21.221,14.95ZM13.4,14.607a2.09,2.09,0,0,0,1.409,1.982,4.7,4.7,0,0,0,1.275.221,1.807,1.807,0,0,0,.9-.151.542.542,0,0,0,.321-.545.558.558,0,0,0-.359-.534,1.2,1.2,0,0,0-.254-.078c-.262-.047-.526-.086-.787-.138a.674.674,0,0,1-.617-.75,3.394,3.394,0,0,1,.218-1.109c.217-.658.509-1.286.79-1.918a15.609,15.609,0,0,0,.745-1.86,1.95,1.95,0,0,0,.06-1.073,1.286,1.286,0,0,0-1.051-1.033,1.977,1.977,0,0,0-1.521.2.339.339,0,0,1-.446-.042c-.1-.092-.2-.189-.307-.284a1.214,1.214,0,0,0-1.643-.061,7.563,7.563,0,0,1-.614.512A.588.588,0,0,1,10.883,8c-.215-.115-.437-.215-.659-.316a2.153,2.153,0,0,0-.695-.248A2.091,2.091,0,0,0,7.541,8.562a9.915,9.915,0,0,0-.405.986c-.559,1.545-1.015,3.123-1.487,4.7a1.528,1.528,0,0,0,.634,1.777,1.755,1.755,0,0,0,1.5.211,1.35,1.35,0,0,0,.824-.858c.543-1.281,1.032-2.584,1.55-3.875.142-.355.28-.712.432-1.064a.548.548,0,0,1,.851-.24.622.622,0,0,1,.185.539,2.161,2.161,0,0,1-.181.621c-.337.852-.68,1.7-1.018,2.552a2.564,2.564,0,0,0-.173.528.624.624,0,0,0,.333.71,1.073,1.073,0,0,0,.814.034,1.22,1.22,0,0,0,.657-.655q.758-1.488,1.511-2.978.35-.687.709-1.37a1.073,1.073,0,0,1,.357-.434.43.43,0,0,1,.463-.016.373.373,0,0,1,.153.387.7.7,0,0,1-.057.236c-.065.157-.127.316-.2.469-.42.883-.846,1.763-1.262,2.648A2.463,2.463,0,0,0,13.4,14.607Zm5.888,6.508a1.09,1.09,0,0,0-2.179.006,1.09,1.09,0,0,0,2.179-.006ZM1.028,12.139a1.038,1.038,0,1,0,.01-2.075,1.038,1.038,0,0,0-.01,2.075ZM13.782.528a1.027,1.027,0,1,0-.011,2.055A1.027,1.027,0,0,0,13.782.528ZM22.21,6.95a.882.882,0,0,0-1.763.011A.882.882,0,0,0,22.21,6.95ZM4.153,4.439a.785.785,0,1,0,.787-.78A.766.766,0,0,0,4.153,4.439Zm8.221,18.22a.676.676,0,1,0-.677.666A.671.671,0,0,0,12.374,22.658ZM22.872,12.2a.674.674,0,0,0-.665.665.656.656,0,0,0,.655.643.634.634,0,0,0,.655-.644A.654.654,0,0,0,22.872,12.2ZM7.171-.123A.546.546,0,0,0,6.613.43a.553.553,0,1,0,1.106,0A.539.539,0,0,0,7.171-.123ZM24.119,9.234a.507.507,0,0,0-.493.488.494.494,0,0,0,.494.494.48.48,0,0,0,.487-.483A.491.491,0,0,0,24.119,9.234Zm-19.454,9.7a.5.5,0,0,0-.488-.488.491.491,0,0,0-.487.5.483.483,0,0,0,.491.479A.49.49,0,0,0,4.665,18.936Z"
transform="translate(0 0.123)"
fill="#f64060"
/>
</svg>
</a>
<a
class="circle-link"
title="Join the Conversation on Discord"
href="https://discord.gg/angular"
target="_blank"
rel="noopener"
>
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 245 240">
<title>Discord Logo</title>
<path
d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"
/>
<path
d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"
/>
</svg>
</a>
</div>
<!-- Footer -->
<footer>
Love Angular?&nbsp;
<a href="https://github.com/angular/angular" target="_blank" rel="noopener">
Give our repo a star.
<div class="github-star-badge">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"
/>
</svg>
Star
</div>
</a>
<a href="https://github.com/angular/angular" target="_blank" rel="noopener">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="#1976d2" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
</a>
</footer>
<svg
id="clouds"
xmlns="http://www.w3.org/2000/svg"
width="2611.084"
height="485.677"
viewBox="0 0 2611.084 485.677"
>
<title>Gray Clouds Background</title>
<path
id="Path_39"
data-name="Path 39"
d="M2379.709,863.793c10-93-77-171-168-149-52-114-225-105-264,15-75,3-140,59-152,133-30,2.83-66.725,9.829-93.5,26.25-26.771-16.421-63.5-23.42-93.5-26.25-12-74-77-130-152-133-39-120-212-129-264-15-54.084-13.075-106.753,9.173-138.488,48.9-31.734-39.726-84.4-61.974-138.487-48.9-52-114-225-105-264,15a162.027,162.027,0,0,0-103.147,43.044c-30.633-45.365-87.1-72.091-145.206-58.044-52-114-225-105-264,15-75,3-140,59-152,133-53,5-127,23-130,83-2,42,35,72,70,86,49,20,106,18,157,5a165.625,165.625,0,0,0,120,0c47,94,178,113,251,33,61.112,8.015,113.854-5.72,150.492-29.764a165.62,165.62,0,0,0,110.861-3.236c47,94,178,113,251,33,31.385,4.116,60.563,2.495,86.487-3.311,25.924,5.806,55.1,7.427,86.488,3.311,73,80,204,61,251-33a165.625,165.625,0,0,0,120,0c51,13,108,15,157-5a147.188,147.188,0,0,0,33.5-18.694,147.217,147.217,0,0,0,33.5,18.694c49,20,106,18,157,5a165.625,165.625,0,0,0,120,0c47,94,178,113,251,33C2446.709,1093.793,2554.709,922.793,2379.709,863.793Z"
transform="translate(142.69 -634.312)"
fill="#eee"
/>
</svg>
</div>
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<router-outlet></router-outlet>

View File

@@ -0,0 +1,33 @@
import { TestBed } from "@angular/core/testing";
import { RouterTestingModule } from "@angular/router/testing";
import { AppComponent } from "./app.component";
describe("AppComponent", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent],
}).compileComponents();
});
it("should create the app", () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'oss-vault'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual("oss-vault");
});
it("should render title", () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector(".content span")?.textContent).toContain(
"oss-vault app is running!"
);
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from "@angular/core";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent {
title = "oss-vault";
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

View File

@@ -0,0 +1,3 @@
export const environment = {
production: true,
};

View File

@@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>OssVault</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,13 @@
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";
import { environment } from "./environments/environment";
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));

View File

@@ -0,0 +1,64 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* IE11 requires the following for NgClass support on SVG elements
*/
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import "zone.js"; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

View File

@@ -0,0 +1,29 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import "zone.js/testing";
import { getTestBed } from "@angular/core/testing";
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from "@angular/platform-browser-dynamic/testing";
declare const require: {
context(
path: string,
deep?: boolean,
filter?: RegExp
): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: true },
});
// Then we find all the tests.
const context = require.context("./", true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/app",
"types": []
},
"files": ["src/main.ts", "src/polyfills.ts"],
"include": ["src/**/*.d.ts"]
}

View File

@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": ["jasmine"]
},
"files": ["src/test.ts", "src/polyfills.ts"],
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}

View File

@@ -0,0 +1,25 @@
# Web
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.0.
## Code scaffolding
Run `ng generate component component-name --project web` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project web`.
> Note: Don't forget to add `--project web` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build web` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build web`, go to the dist folder `cd dist/web` and run `npm publish`.
## Running unit tests
Run `ng test web` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@@ -0,0 +1,41 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: "",
frameworks: ["jasmine", "@angular-devkit/build-angular"],
plugins: [
require("karma-jasmine"),
require("karma-chrome-launcher"),
require("karma-jasmine-html-reporter"),
require("karma-coverage"),
require("@angular-devkit/build-angular/plugins/karma"),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter: {
dir: require("path").join(__dirname, "../../coverage/web"),
subdir: ".",
reporters: [{ type: "html" }, { type: "text-summary" }],
},
reporters: ["progress", "kjhtml"],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ["Chrome"],
singleRun: false,
restartOnFileChange: true,
});
};

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/web",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "@bitwarden/web-vault-internal",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^12.2.0",
"@angular/core": "^12.2.0"
},
"dependencies": {
"tslib": "^2.3.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -23,11 +23,7 @@
<p>{{ "acceptEmergencyAccess" | i18n }}</p>
<hr />
<div class="d-flex">
<a
routerLink="/login"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block"
>
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
{{ "logIn" | i18n }}
</a>
<a

View File

@@ -6,7 +6,6 @@ import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { EmergencyAccessAcceptRequest } from "jslib-common/models/request/emergencyAccessAcceptRequest";
import { BaseAcceptComponent } from "../common/base.accept.component";
@Component({

View File

@@ -24,11 +24,7 @@
<p>{{ "joinOrganizationDesc" | i18n }}</p>
<hr />
<div class="d-flex">
<a
routerLink="/login"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block"
>
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
{{ "logIn" | i18n }}
</a>
<a

View File

@@ -8,11 +8,12 @@ import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { Utils } from "jslib-common/misc/utils";
import { Policy } from "jslib-common/models/domain/policy";
import { OrganizationUserAcceptRequest } from "jslib-common/models/request/organizationUserAcceptRequest";
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
import { Utils } from "jslib-common/misc/utils";
import { Policy } from "jslib-common/models/domain/policy";
import { BaseAcceptComponent } from "../common/base.accept.component";
@Component({

View File

@@ -33,7 +33,7 @@
aria-hidden="true"
></i>
</button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>

View File

@@ -1,12 +1,13 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { HintComponent as BaseHintComponent } from "jslib-angular/components/hint.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { HintComponent as BaseHintComponent } from "jslib-angular/components/hint.component";
@Component({
selector: "app-hint",
templateUrl: "hint.component.html",

View File

@@ -1,7 +1,6 @@
import { Component, NgZone } from "@angular/core";
import { Router } from "@angular/router";
import { LockComponent as BaseLockComponent } from "jslib-angular/components/lock.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
@@ -15,6 +14,8 @@ import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.serv
import { RouterService } from "../services/router.service";
import { LockComponent as BaseLockComponent } from "jslib-angular/components/lock.component";
@Component({
selector: "app-lock",
templateUrl: "lock.component.html",
@@ -55,10 +56,10 @@ export class LockComponent extends BaseLockComponent {
await super.ngOnInit();
this.onSuccessfulSubmit = async () => {
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl && previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
if (previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
this.successRoute = previousUrl;
}
this.router.navigateByUrl(this.successRoute);
this.router.navigate([this.successRoute]);
};
}
}

View File

@@ -86,7 +86,7 @@
[queryParams]="{ email: email }"
class="btn btn-outline-secondary btn-block ml-2 mt-0"
>
<i class="bwi bwi-pencil-square" aria-hidden="true"></i>
<i class="bwi bwi-pencil-square-o" aria-hidden="true"></i>
{{ "createAccount" | i18n }}
</a>
</div>

View File

@@ -0,0 +1,118 @@
import { Component, NgZone } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { LoginComponent as BaseLoginComponent } from "jslib-angular/components/login.component";
import { Policy } from "jslib-common/models/domain/policy";
@Component({
selector: "app-login",
templateUrl: "login.component.html",
})
export class LoginComponent extends BaseLoginComponent {
showResetPasswordAutoEnrollWarning = false;
constructor(
authService: AuthService,
router: Router,
i18nService: I18nService,
private route: ActivatedRoute,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService,
cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
private policyService: PolicyService,
logService: LogService,
ngZone: NgZone
) {
super(
authService,
router,
platformUtilsService,
i18nService,
stateService,
environmentService,
passwordGenerationService,
cryptoFunctionService,
logService,
ngZone
);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}
async ngOnInit() {
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
this.email = qParams.email;
}
if (qParams.premium != null) {
this.stateService.setLoginRedirect({ route: "/settings/premium" });
} else if (qParams.org != null) {
this.stateService.setLoginRedirect({
route: "/settings/create-organization",
qParams: { plan: qParams.org },
});
}
// Are they coming from an email for sponsoring a families organization
if (qParams.sponsorshipToken != null) {
// After logging in redirect them to setup the families sponsorship
this.stateService.setLoginRedirect({
route: "/setup/families-for-enterprise",
qParams: { token: qParams.sponsorshipToken },
});
}
await super.ngOnInit();
});
const invite = await this.stateService.getOrganizationInvitation();
if (invite != null) {
let policyList: Policy[] = null;
try {
const policies = await this.apiService.getPoliciesByToken(
invite.organizationId,
invite.token,
invite.email,
invite.organizationUserId
);
policyList = this.policyService.mapPoliciesFromToken(policies);
} catch (e) {
this.logService.error(e);
}
if (policyList != null) {
const result = this.policyService.getResetPasswordPolicyOptions(
policyList,
invite.organizationId
);
// Set to true if policy enabled and auto-enroll enabled
this.showResetPasswordAutoEnrollWarning = result[1] && result[0].autoEnrollEnabled;
}
}
}
async goAfterLogIn() {
const loginRedirect = await this.stateService.getLoginRedirect();
if (loginRedirect != null) {
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
await this.stateService.setLoginRedirect(null);
} else {
this.router.navigate([this.successRoute]);
}
}
}

View File

@@ -33,7 +33,7 @@
aria-hidden="true"
></i>
</button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>

View File

@@ -5,6 +5,7 @@ import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { DeleteRecoverRequest } from "jslib-common/models/request/deleteRecoverRequest";
@Component({

View File

@@ -7,7 +7,7 @@
<p>
{{ "recoverAccountTwoStepDesc" | i18n }}
<a
href="https://bitwarden.com/help/lost-two-step-device/"
href="https://help.bitwarden.com/article/lost-two-step-device/"
target="_blank"
rel="noopener"
>{{ "learnMore" | i18n }}</a
@@ -65,7 +65,7 @@
aria-hidden="true"
></i>
</button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>

View File

@@ -7,6 +7,7 @@ import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TwoFactorRecoveryRequest } from "jslib-common/models/request/twoFactorRecoveryRequest";
@Component({

View File

@@ -1,100 +1,44 @@
<div class="layout" [ngClass]="['layout', layout]">
<!-- TEAMS 1 Header -->
<header
class="header"
*ngIf="
layout === 'default' ||
layout === 'teams' ||
layout === 'teams1' ||
layout === 'teams2' ||
layout === 'enterprise' ||
layout === 'enterprise1' ||
layout === 'enterprise2' ||
layout === 'cnetcmpgnent' ||
layout === 'cnetcmpgnteams' ||
layout === 'cnetcmpgnind'
"
>
<header class="header" *ngIf="layout === 'enterprise2'">
<div class="container">
<div class="row">
<div class="col-7">
<img
alt="Bitwarden"
class="logo mb-2"
src="../../images/register-layout/logo-horizontal-white.svg"
src="../../images/register-layout/logo-horizontal-white.png"
/>
</div>
</div>
</div>
</header>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row">
<div class="col-7" *ngIf="layout">
<div class="mt-5">
<!-- Default Body -->
<div
*ngIf="
layout === 'teams' ||
layout === 'enterprise' ||
layout === 'enterprise1' ||
layout === 'default'
"
>
<h1>The Bitwarden Password Manager</h1>
<h2>
Trusted by millions of individuals, teams, and organizations worldwide for secure
password storage and sharing.
</h2>
<p>Store logins, secure notes, and more</p>
<div *ngIf="layout === 'enterprise2'">
<h2>Companies globally trust Bitwarden for password management.</h2>
<p>Start your 7-day free trial!</p>
<p class="highlight">Quickly deploy your <b>organization</b></p>
<p>Use Bitwarden across all platforms</p>
<p>Collaborate and share securely</p>
<p>Access anywhere on any device</p>
<p>Create your account to get started</p>
<figure>
<figcaption>
<cite>
<img src="../../images/register-layout/wired-logo.png" alt="Wired" />
</cite>
</figcaption>
<blockquote>
"Bitwarden has become a popular choice among open-source software advocates. After
using it for a few months, I can see why." - February 2020
</blockquote>
</figure>
</div>
<!-- Teams & Enterprise Body -->
<div *ngIf="layout === 'teams1' || layout === 'teams2' || layout === 'enterprise2'">
<h1>
Start Your <span *ngIf="layout === 'teams1' || layout === 'teams1'">Teams<br /></span
><span *ngIf="layout === 'enterprise2'">Enterprise</span> Free Trial Now
</h1>
<h2>
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure
password storage and sharing.
</h2>
<p>Collaborate and share securely</p>
<p>Deploy and manage quickly and easily</p>
<p>Access anywhere on any device</p>
<p>Create your account to get started</p>
<div *ngIf="layout === 'enterprise3'">
<p>Enterprise 3 layout</p>
</div>
<!-- CNET Campaign Teams & Enterprise Body -->
<div *ngIf="layout === 'cnetcmpgnteams' || layout === 'cnetcmpgnent'">
<h1>
Start Your <span *ngIf="layout === 'cnetcmpgnteams'">Teams<br /></span
><span *ngIf="layout === 'cnetcmpgnent'">Enterprise</span> Free Trial Now
</h1>
<h2>
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure
password storage and sharing.
</h2>
<p>Collaborate and share securely</p>
<p>Deploy and manage quickly and easily</p>
<p>Access anywhere on any device</p>
<p>Create your account to get started</p>
</div>
<!-- CNET Campaign Premium Body -->
<div *ngIf="layout === 'cnetcmpgnind'">
<h1>Start Your Premium Account Now</h1>
<h2>
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure
password storage and sharing.
</h2>
<p>Store logins, secure notes, and more</p>
<p>Secure your account with advanced two-step login</p>
<p>Access anywhere on any device</p>
<p>Create your account to get started</p>
<div *ngIf="layout === 'enterprise4'">
<p>Enterprise 4 layout</p>
</div>
</div>
</div>
@@ -258,7 +202,7 @@
aria-hidden="true"
></i>
</button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
@@ -268,88 +212,5 @@
</div>
</div>
</div>
<div class="row">
<div class="col-7 d-flex align-items-center">
<div
*ngIf="
layout === 'cnetcmpgnent' || layout === 'cnetcmpgnteams' || layout === 'cnetcmpgnind'
"
>
<figure>
<figcaption>
<cite>
<img
src="../../images/register-layout/cnet-logo.svg"
class="w-25 d-block mx-auto"
alt="cnet logo"
/>
</cite>
</figcaption>
<blockquote class="mx-auto text-center px-4">
"No more excuses; start using Bitwarden today. The identity you save could be your
own. The money definitely will be."
</blockquote>
</figure>
</div>
<div
*ngIf="
layout === 'teams' ||
layout === 'teams1' ||
layout === 'teams2' ||
layout === 'enterprise' ||
layout === 'enterprise1' ||
layout === 'enterprise2' ||
layout === 'default'
"
>
<figure>
<figcaption>
<cite>
<img
src="../../images/register-layout/forbes-logo.svg"
class="w-25 d-block mx-auto"
alt="Forbes Logo"
/>
</cite>
</figcaption>
<blockquote class="mx-auto text-center px-4">
“Bitwarden boasts the backing of some of the world's best security experts and an
attractive, easy-to-use interface”
</blockquote>
</figure>
</div>
</div>
<div
*ngIf="
layout === 'cnetcmpgnent' || layout === 'cnetcmpgnteams' || layout === 'cnetcmpgnind'
"
class="col-5 d-flex align-items-center justify-content-center"
>
<img
src="../../images/register-layout/usnews-360-badge.svg"
class="w-50 d-block"
alt="US News 360 Reviews Best Password Manager"
/>
</div>
<div
*ngIf="
layout === 'teams' ||
layout === 'teams1' ||
layout === 'teams2' ||
layout === 'enterprise' ||
layout === 'enterprise1' ||
layout === 'enterprise2' ||
layout === 'default'
"
class="col-5 d-flex align-items-center justify-content-center"
>
<img
src="../../images/register-layout/usnews-360-badge.svg"
class="w-50 d-block"
alt="US News 360 Reviews Best Password Manager"
/>
</div>
</div>
</form>
</div>

View File

@@ -1,8 +1,8 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { RegisterComponent as BaseRegisterComponent } from "jslib-angular/components/register.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
@@ -13,12 +13,14 @@ import { PasswordGenerationService } from "jslib-common/abstractions/passwordGen
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { PolicyData } from "jslib-common/models/data/policyData";
import { RegisterComponent as BaseRegisterComponent } from "jslib-angular/components/register.component";
import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPasswordPolicyOptions";
import { Policy } from "jslib-common/models/domain/policy";
import { ReferenceEventRequest } from "jslib-common/models/request/referenceEventRequest";
import { RouterService } from "../services/router.service";
import { PolicyData } from "jslib-common/models/data/policyData";
import { ReferenceEventRequest } from "jslib-common/models/request/referenceEventRequest";
@Component({
selector: "app-register",
@@ -43,8 +45,7 @@ export class RegisterComponent extends BaseRegisterComponent {
passwordGenerationService: PasswordGenerationService,
private policyService: PolicyService,
environmentService: EnvironmentService,
logService: LogService,
private routerService: RouterService
logService: LogService
) {
super(
authService,
@@ -67,14 +68,14 @@ export class RegisterComponent extends BaseRegisterComponent {
this.email = qParams.email;
}
if (qParams.premium != null) {
this.routerService.setPreviousUrl("/settings/premium");
this.stateService.setLoginRedirect({ route: "/settings/premium" });
} else if (qParams.org != null) {
this.showCreateOrgMessage = true;
this.referenceData.flow = qParams.org;
const route = this.router.createUrlTree(["create-organization"], {
queryParams: { plan: qParams.org },
this.stateService.setLoginRedirect({
route: "/settings/create-organization",
qParams: { plan: qParams.org },
});
this.routerService.setPreviousUrl(route.toString());
}
if (qParams.layout != null) {
this.layout = this.referenceData.layout = qParams.layout;
@@ -91,10 +92,10 @@ export class RegisterComponent extends BaseRegisterComponent {
// Are they coming from an email for sponsoring a families organization
if (qParams.sponsorshipToken != null) {
// After logging in redirect them to setup the families sponsorship
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
queryParams: { plan: qParams.sponsorshipToken },
this.stateService.setLoginRedirect({
route: "/setup/families-for-enterprise",
qParams: { token: qParams.sponsorshipToken },
});
this.routerService.setPreviousUrl(route.toString());
}
if (this.referenceData.id === "") {
this.referenceData.id = null;

View File

@@ -1,7 +1,6 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SetPasswordComponent as BaseSetPasswordComponent } from "jslib-angular/components/set-password.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@@ -12,6 +11,8 @@ import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { SetPasswordComponent as BaseSetPasswordComponent } from "jslib-angular/components/set-password.component";
@Component({
selector: "app-set-password",
templateUrl: "set-password.component.html",

View File

@@ -41,7 +41,7 @@
aria-hidden="true"
></i>
</button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>

View File

@@ -1,8 +1,8 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { SsoComponent as BaseSsoComponent } from "jslib-angular/components/sso.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
@@ -13,6 +13,8 @@ import { PasswordGenerationService } from "jslib-common/abstractions/passwordGen
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SsoComponent as BaseSsoComponent } from "jslib-angular/components/sso.component";
@Component({
selector: "app-sso",
templateUrl: "sso.component.html",

View File

@@ -1,10 +1,11 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "jslib-angular/components/two-factor-options.component";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "jslib-angular/components/two-factor-options.component";
@Component({
selector: "app-two-factor-options",
@@ -12,11 +13,11 @@ import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
})
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
constructor(
twoFactorService: TwoFactorService,
authService: AuthService,
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService
) {
super(twoFactorService, router, i18nService, platformUtilsService, window);
super(authService, router, i18nService, platformUtilsService, window);
}
}

View File

@@ -114,9 +114,6 @@
<p>{{ "noTwoStepProviders2" | i18n }}</p>
</ng-container>
<hr />
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<div class="d-flex mb-3">
<button
type="submit"
@@ -138,7 +135,7 @@
aria-hidden="true"
></i>
</button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>

View File

@@ -1,20 +1,20 @@
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorComponent as BaseTwoFactorComponent } from "jslib-angular/components/two-factor.component";
import { ModalService } from "jslib-angular/services/modal.service";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AppIdService } from "jslib-common/abstractions/appId.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { ModalService } from "jslib-angular/services/modal.service";
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { RouterService } from "../services/router.service";
import { TwoFactorComponent as BaseTwoFactorComponent } from "jslib-angular/components/two-factor.component";
import { TwoFactorOptionsComponent } from "./two-factor-options.component";
@@ -36,10 +36,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
environmentService: EnvironmentService,
private modalService: ModalService,
route: ActivatedRoute,
logService: LogService,
twoFactorService: TwoFactorService,
appIdService: AppIdService,
private routerService: RouterService
logService: LogService
) {
super(
authService,
@@ -51,9 +48,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
environmentService,
stateService,
route,
logService,
twoFactorService,
appIdService
logService
);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}
@@ -76,9 +71,10 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
}
async goAfterLogIn() {
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl) {
this.router.navigateByUrl(previousUrl);
const loginRedirect = await this.stateService.getLoginRedirect();
if (loginRedirect != null) {
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
await this.stateService.setLoginRedirect(null);
} else {
this.router.navigate([this.successRoute], {
queryParams: {

View File

@@ -1,6 +1,5 @@
import { Component } from "@angular/core";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "jslib-angular/components/update-temp-password.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@@ -9,9 +8,11 @@ import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "jslib-angular/components/update-temp-password.component";
import { StateService } from "jslib-common/abstractions/state.service";
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",

View File

@@ -1,5 +1,6 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { ApiService } from "jslib-common/abstractions/api.service";
@@ -7,6 +8,7 @@ import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { VerifyEmailRequest } from "jslib-common/models/request/verifyEmailRequest";
@Component({

View File

@@ -23,7 +23,7 @@
aria-hidden="true"
></i>
</button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>

View File

@@ -1,11 +1,13 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { VerifyDeleteRecoverRequest } from "jslib-common/models/request/verifyDeleteRecoverRequest";
@Component({

View File

@@ -0,0 +1,10 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@@ -0,0 +1,31 @@
import { TestBed } from "@angular/core/testing";
import { RouterTestingModule } from "@angular/router/testing";
import { AppComponent } from "./app.component";
describe("AppComponent", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent],
}).compileComponents();
});
it("should create the app", () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'lawl'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual("lawl");
});
it("should render title", () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector(".content span")?.textContent).toContain("lawl app is running!");
});
});

Some files were not shown because too many files have changed in this diff Show More